diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b77efb6b..92b87569c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,8 +54,12 @@ Some services will be required to pass the entire test suite. We recommend using docker run --name mongodb -d -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=user -e MONGO_INITDB_ROOT_PASSWORD=password mongodb/mongodb-community-server:latest docker run -d -p 21:21 -p 21000-21010:21000-21010 -e USERS='user|password' delfer/alpine-ftp-server +// the docker image is relatively unstable. Alternatively, refer to official guide of OpenTSDB to locally setup OpenTSDB env. +// http://opentsdb.net/docs/build/html/installation.html#id1 +docker run -d --name gofr-opentsdb -p 4242:4242 petergrace/opentsdb-docker:latest docker run --name gofr-mysql -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=test -p 2001:3306 -d mysql:8.0.30 docker run --name gofr-redis -p 2002:6379 -d redis:7.0.5 +docker run --name gofr-solr -p 2020:8983 solr -DzkRun docker run --name gofr-zipkin -d -p 2005:9411 openzipkin/zipkin:2 docker run --rm -it -p 4566:4566 -p 4510-4559:4510-4559 localstack/localstack docker run --name cassandra-node -d -p 9042:9042 -v cassandra_data:/var/lib/cassandra cassandra:latest diff --git a/docs/advanced-guide/injecting-databases-drivers/page.md b/docs/advanced-guide/injecting-databases-drivers/page.md index c36ab6bcb..426bad20a 100644 --- a/docs/advanced-guide/injecting-databases-drivers/page.md +++ b/docs/advanced-guide/injecting-databases-drivers/page.md @@ -20,6 +20,13 @@ type Clickhouse interface { User's can easily inject a driver that supports this interface, this provides usability without compromising the extensibility to use multiple databases. + +Import the gofr's external driver for ClickHouse: + +```shell +go get gofr.dev/pkg/gofr/datasource/clickhouse@latest +``` + ### Example ```go package main @@ -104,6 +111,13 @@ type Mongo interface { User's can easily inject a driver that supports this interface, this provides usability without compromising the extensibility to use multiple databases. + +Import the gofr's external driver for MongoDB: + +```shell +go get gofr.dev/pkg/gofr/datasource/mongo@latest +``` + ### Example ```go package main @@ -124,7 +138,7 @@ type Person struct { func main() { app := gofr.New() - db := mongo.New(Config{URI: "mongodb://localhost:27017", Database: "test"}) + db := mongo.New(Config{URI: "mongodb://localhost:27017", Database: "test",ConnectionTimeout: 4*time.Second}) // inject the mongo into gofr to use mongoDB across the application // using gofr context @@ -197,6 +211,12 @@ type CassandraBatchWithContext interface { GoFr simplifies Cassandra integration with a well-defined interface. Users can easily implement any driver that adheres to this interface, fostering a user-friendly experience. +Import the gofr's external driver for Cassandra: + +```shell +go get gofr.dev/pkg/gofr/datasource/cassandra@latest +``` + ### Example ```go @@ -291,6 +311,12 @@ type Dgraph interface { Users can easily inject a driver that supports this interface, allowing for flexibility without compromising usability. This structure supports both queries and mutations in Dgraph. +Import the gofr's external driver for DGraph: + +```shell +go get gofr.dev/pkg/gofr/datasource/dgraph@latest +``` + ### Example ```go @@ -408,6 +434,15 @@ type Solr interface { User's can easily inject a driver that supports this interface, this provides usability without compromising the extensibility to use multiple databases. +Import the gofr's external driver for Solr: + +```shell +go get gofr.dev/pkg/gofr/datasource/solr@latest +``` +Note : This datasource package requires the user to create the collection before performing any operations. +While testing the below code create a collection using : +`curl --location 'http://localhost:2020/solr/admin/collections?action=CREATE&name=test&numShards=2&replicationFactor=1&wt=xml'` + ```go package main @@ -440,7 +475,7 @@ type Person struct { } func post(c *gofr.Context) (interface{}, error) { - p := Person{Name: "Srijan", Age: 24} + p := []Person{{Name: "Srijan", Age: 24}} body, _ := json.Marshal(p) resp, err := c.Solr.Create(c, "test", bytes.NewBuffer(body), nil) @@ -471,3 +506,210 @@ func get(c *gofr.Context) (interface{}, error) { return resp, nil } ``` + +## OpenTSDB +GoFr supports injecting OpenTSDB to facilitate interaction with OpenTSDB's REST APIs. +Implementations adhering to the `OpenTSDB` interface can be registered with `app.AddOpenTSDB()`, +enabling applications to leverage OpenTSDB for time-series data management through `gofr.Context`. + +```go +// OpenTSDB provides methods for GoFr applications to communicate with OpenTSDB +// through its REST APIs. +type OpenTSDB interface { + +// HealthChecker verifies if the OpenTSDB server is reachable. +// Returns an error if the server is unreachable, otherwise nil. +HealthChecker + +// PutDataPoints sends data to store metrics in OpenTSDB. +// +// Parameters: +// - ctx: Context for managing request lifetime. +// - data: A slice of DataPoint objects; must contain at least one entry. +// - queryParam: Specifies the response format: +// - client.PutRespWithSummary: Requests a summary response. +// - client.PutRespWithDetails: Requests detailed response information. +// - Empty string (""): No additional response details. +// - res: A pointer to PutResponse, where the server's response will be stored. +// +// Returns: +// - Error if parameters are invalid, response parsing fails, or if connectivity issues occur. +PutDataPoints(ctx context.Context, data any, queryParam string, res any) error + +// QueryDataPoints retrieves data based on the specified parameters. +// +// Parameters: +// - ctx: Context for managing request lifetime. +// - param: An instance of QueryParam with query parameters for filtering data. +// - res: A pointer to QueryResponse, where the server's response will be stored. +QueryDataPoints(ctx context.Context, param any, res any) error + +// QueryLatestDataPoints fetches the latest data point(s). +// +// Parameters: +// - ctx: Context for managing request lifetime. +// - param: An instance of QueryLastParam with query parameters for the latest data point. +// - res: A pointer to QueryLastResponse, where the server's response will be stored. +QueryLatestDataPoints(ctx context.Context, param any, res any) error + +// GetAggregators retrieves available aggregation functions. +// +// Parameters: +// - ctx: Context for managing request lifetime. +// - res: A pointer to AggregatorsResponse, where the server's response will be stored. +GetAggregators(ctx context.Context, res any) error + +// QueryAnnotation retrieves a single annotation. +// +// Parameters: +// - ctx: Context for managing request lifetime. +// - queryAnnoParam: A map of parameters for the annotation query, such as client.AnQueryStartTime, client.AnQueryTSUid. +// - res: A pointer to AnnotationResponse, where the server's response will be stored. +QueryAnnotation(ctx context.Context, queryAnnoParam map[string]any, res any) error + +// PostAnnotation creates or updates an annotation. +// +// Parameters: +// - ctx: Context for managing request lifetime. +// - annotation: The annotation to be created or updated. +// - res: A pointer to AnnotationResponse, where the server's response will be stored. +PostAnnotation(ctx context.Context, annotation any, res any) error + +// PutAnnotation creates or replaces an annotation. +// Fields not included in the request will be reset to default values. +// +// Parameters: +// - ctx: Context for managing request lifetime. +// - annotation: The annotation to be created or replaced. +// - res: A pointer to AnnotationResponse, where the server's response will be stored. +PutAnnotation(ctx context.Context, annotation any, res any) error + +// DeleteAnnotation removes an annotation. +// +// Parameters: +// - ctx: Context for managing request lifetime. +// - annotation: The annotation to be deleted. +// - res: A pointer to AnnotationResponse, where the server's response will be stored. +DeleteAnnotation(ctx context.Context, annotation any, res any) error +} +``` + +Import the gofr's external driver for OpenTSDB: + +```go +go get gofr.dev/pkg/gofr/datasource/opentsdb +``` + +The following example demonstrates injecting an OpenTSDB instance into a GoFr application +and using it to perform a health check on the OpenTSDB server. +```go +package main + +import ( + "context" + "fmt" + "math/rand/v2" + "time" + + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/datasource/opentsdb" +) + +func main() { + app := gofr.New() + + // Initialize OpenTSDB connection + app.AddOpenTSDB(opentsdb.New(&opentsdb.Config{ + Host: "localhost:4242", + MaxContentLength: 4096, + MaxPutPointsNum: 1000, + DetectDeltaNum: 10, + })) + + // Register routes + app.GET("/health", opentsdbHealthCheck) + app.POST("/write", writeDataPoints) + app.GET("/query", queryDataPoints) + // Run the app + app.Run() +} + +// Health check for OpenTSDB +func opentsdbHealthCheck(c *gofr.Context) (any, error) { + res, err := c.OpenTSDB.HealthCheck(context.Background()) + if err != nil { + return nil, err + } + return res, nil +} + +// Write Data Points to OpenTSDB +func writeDataPoints(c *gofr.Context) (any, error) { + PutDataPointNum := 4 + name := []string{"cpu", "disk", "net", "mem"} + cpuDatas := make([]opentsdb.DataPoint, 0) + + tags := map[string]string{ + "host": "gofr-host", + "try-name": "gofr-sample", + "demo-name": "opentsdb-test", + } + + for i := 0; i < PutDataPointNum; i++ { + data := opentsdb.DataPoint{ + Metric: name[i%len(name)], + Timestamp: time.Now().Unix(), + Value: rand.Float64() * 100, + Tags: tags, + } + cpuDatas = append(cpuDatas, data) + } + + resp := opentsdb.PutResponse{} + + err := c.OpenTSDB.PutDataPoints(context.Background(), cpuDatas, "details", &resp) + if err != nil { + return resp.Errors, err + } + + return fmt.Sprintf("%v Data points written successfully", resp.Success), nil +} + +// Query Data Points from OpenTSDB +func queryDataPoints(c *gofr.Context) (any, error) { + st1 := time.Now().Unix() - 3600 + st2 := time.Now().Unix() + + queryParam := opentsdb.QueryParam{ + Start: st1, + End: st2, + } + + name := []string{"cpu", "disk", "net", "mem"} + subqueries := make([]opentsdb.SubQuery, 0) + tags := map[string]string{ + "host": "gofr-host", + "try-name": "gofr-sample", + "demo-name": "opentsdb-test", + } + + for _, metric := range name { + subQuery := opentsdb.SubQuery{ + Aggregator: "sum", + Metric: metric, + Tags: tags, + } + subqueries = append(subqueries, subQuery) + } + + queryParam.Queries = subqueries + + queryResp := &opentsdb.QueryResponse{} + + err := c.OpenTSDB.QueryDataPoints(c, &queryParam, queryResp) + if err != nil { + return nil, err + } + return queryResp.QueryRespCnts, nil +} +``` \ No newline at end of file diff --git a/docs/advanced-guide/key-value-store/page.md b/docs/advanced-guide/key-value-store/page.md index f5f09c608..b9ff0355d 100644 --- a/docs/advanced-guide/key-value-store/page.md +++ b/docs/advanced-guide/key-value-store/page.md @@ -22,6 +22,13 @@ using `app.AddKVStore()` method, and user's can use BadgerDB across application User's can easily inject a driver that supports this interface, this provides usability without compromising the extensibility to use multiple databases. + +Import the gofr's external driver for BadgerDB: + +```go +go get gofr.dev/pkg/gofr/datasource/kv-store/badger +``` + ### Example ```go package main diff --git a/docs/advanced-guide/middlewares/page.md b/docs/advanced-guide/middlewares/page.md index 7369757bb..4ddcf023f 100644 --- a/docs/advanced-guide/middlewares/page.md +++ b/docs/advanced-guide/middlewares/page.md @@ -67,56 +67,3 @@ func main() { } ``` -### Using UseMiddlewareWithContainer for Custom Middleware with Container Access - -The UseMiddlewareWithContainer method allows middleware to access the application's container, providing access to -services like logging, configuration, and databases. This method is especially useful for middleware that needs access -to resources in the container to modify request processing flow. - -#### Example: - -```go -import ( - "fmt" - "net/http" - - "gofr.dev/pkg/gofr" - "gofr.dev/pkg/gofr/container" -) - -func main() { - // Create a new GoFr application instance - a := gofr.New() - - // Add custom middleware with container access - a.UseMiddlewareWithContainer(customMiddleware) - - // Define the application's routes - a.GET("/hello", HelloHandler) - - // Run the application - a.Run() -} - -// Define middleware with container access -func customMiddleware(c *container.Container, handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c.Logger.Log("Hey! Welcome to GoFr") - - // Continue with the request processing - handler.ServeHTTP(w, r) - }) -} - -// Sample handler function -func HelloHandler(c *gofr.Context) (interface{}, error) { - name := c.Param("name") - if name == "" { - c.Log("Name came empty") - name = "World" - } - - return fmt.Sprintf("Hello %s!", name), nil -} -``` - diff --git a/docs/advanced-guide/setting-custom-response-headers/page.md b/docs/advanced-guide/setting-custom-response-headers/page.md new file mode 100644 index 000000000..e7c21e289 --- /dev/null +++ b/docs/advanced-guide/setting-custom-response-headers/page.md @@ -0,0 +1,56 @@ +# Setting Custom Response Headers + +In GoFr, you can customize HTTP response headers using the `Response` struct, allowing you to add extra information to +responses sent from your application. This feature can be useful for adding metadata, such as custom headers, security +policies, or other contextual information, to improve the client-server communication. + +## Using the Response Struct + +To use custom headers in your handler, create and return a Response object within the handler function. This object +should contain the response data along with a Headers map for any custom headers you wish to add. + +### Example: + +Below is an example showing how to use the Response struct in a GoFr handler. In this case, the `HelloHandler` function +returns a greeting message along with two custom headers: X-Custom-Header and X-Another-Header. + +```go +package main + +import ( + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/http/response" +) + +func main() { + // Create a new application + a := gofr.New() + + // Add the route + a.GET("/hello", HelloHandler) + + // Run the application + a.Run() +} + +func HelloHandler(c *gofr.Context) (interface{}, error) { + name := c.Param("name") + if name == "" { + c.Log("Name came empty") + name = "World" + } + + headers := map[string]string{ + "X-Custom-Header": "CustomValue", + "X-Another-Header": "AnotherValue", + } + + return response.Response{ + Data: "Hello World from new Server", + Headers: headers, + }, nil +} +``` + +This functionality offers a convenient, structured way to include additional response information without altering the +core data payload. \ No newline at end of file diff --git a/docs/navigation.js b/docs/navigation.js index 4c502fd7e..3daf62813 100644 --- a/docs/navigation.js +++ b/docs/navigation.js @@ -57,6 +57,11 @@ export const navigation = [ href: '/docs/advanced-guide/publishing-custom-metrics', desc: "Explore methods for publishing custom metrics to monitor your application's performance and gain valuable insights." }, + { + title: 'Custom Headers in Response', + href: '/docs/advanced-guide/setting-custom-response-headers', + desc: "Learn how to include custom headers in HTTP responses to provide additional context and control to your API clients." + }, { title: 'Custom Spans in Tracing', href: '/docs/advanced-guide/custom-spans-in-tracing', diff --git a/docs/quick-start/connecting-mysql/page.md b/docs/quick-start/connecting-mysql/page.md index c5674db0b..6f8123f29 100644 --- a/docs/quick-start/connecting-mysql/page.md +++ b/docs/quick-start/connecting-mysql/page.md @@ -1,6 +1,6 @@ -# Connecting MySQL +# Connecting MySQL/MariaDB -Just like Redis GoFr also supports connection to SQL(MySQL and Postgres) databases based on configuration variables. +Just like Redis GoFr also supports connection to SQL(MySQL, MariaDB and Postgres) databases based on configuration variables. ## Setup @@ -10,17 +10,24 @@ Users can run MySQL and create a database locally using the following Docker com docker run --name gofr-mysql -e MYSQL_ROOT_PASSWORD=root123 -e MYSQL_DATABASE=test_db -p 3306:3306 -d mysql:8.0.30 ``` -Access `test_db` database and create table customer with columns `id` and `name` +For MariaDB you would run: + +```bash +docker run --name gofr-mariadb -e MYSQL_ROOT_PASSWORD=root123 -e MYSQL_DATABASE=test_db -p 3306:3306 -d mariadb:latest +``` + + +Access `test_db` database and create table customer with columns `id` and `name`. Change mysql to mariadb as needed: ```bash docker exec -it gofr-mysql mysql -uroot -proot123 test_db -e "CREATE TABLE customers (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL);" ``` -Now the database with table is ready, we can connect our GoFr server to MySQL +Now the database with table is ready, we can connect our GoFr server to MySQL/MariaDB. ## Configuration & Usage -After adding MySQL configs `.env` will be updated to the following. +After adding MySQL/MariaDB configs `.env` will be updated to the following. Use ```DB_DIALECT=mysql``` for both MySQL and MariaDB. ```dotenv # configs/.env @@ -46,7 +53,7 @@ DB_CHARSET= Now in the following example, we'll store customer data using **POST** `/customer` and then use **GET** `/customer` to retrieve the same. We will be storing the customer data with `id` and `name`. -After adding code to add and retrieve data from MySQL datastore, `main.go` will be updated to the following. +After adding code to add and retrieve data from MySQL/MariaDB datastore, `main.go` will be updated to the following. ```go package main diff --git a/go.mod b/go.mod index 31cf72ef4..45a408c7d 100644 --- a/go.mod +++ b/go.mod @@ -23,34 +23,34 @@ require ( github.com/redis/go-redis/extra/redisotel/v9 v9.7.0 github.com/redis/go-redis/v9 v9.7.0 github.com/segmentio/kafka-go v0.4.47 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 - go.opentelemetry.io/otel v1.31.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 + go.opentelemetry.io/otel v1.32.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 go.opentelemetry.io/otel/exporters/prometheus v0.52.0 - go.opentelemetry.io/otel/exporters/zipkin v1.31.0 - go.opentelemetry.io/otel/metric v1.31.0 - go.opentelemetry.io/otel/sdk v1.31.0 + go.opentelemetry.io/otel/exporters/zipkin v1.32.0 + go.opentelemetry.io/otel/metric v1.32.0 + go.opentelemetry.io/otel/sdk v1.32.0 go.opentelemetry.io/otel/sdk/metric v1.30.0 - go.opentelemetry.io/otel/trace v1.31.0 + go.opentelemetry.io/otel/trace v1.32.0 go.uber.org/mock v0.5.0 golang.org/x/oauth2 v0.24.0 - golang.org/x/sync v0.8.0 - golang.org/x/term v0.25.0 - golang.org/x/text v0.19.0 - google.golang.org/api v0.203.0 + golang.org/x/sync v0.9.0 + golang.org/x/term v0.26.0 + golang.org/x/text v0.20.0 + google.golang.org/api v0.209.0 google.golang.org/grpc v1.67.1 - google.golang.org/protobuf v1.35.1 - modernc.org/sqlite v1.33.1 + google.golang.org/protobuf v1.35.2 + modernc.org/sqlite v1.34.1 ) require ( cloud.google.com/go v0.116.0 // indirect - cloud.google.com/go/auth v0.9.9 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect + cloud.google.com/go/auth v0.10.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.5 // indirect cloud.google.com/go/compute/metadata v0.5.2 // indirect - cloud.google.com/go/iam v1.2.1 // indirect + cloud.google.com/go/iam v1.2.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -66,8 +66,8 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect - github.com/googleapis/gax-go/v2 v2.13.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/googleapis/gax-go/v2 v2.14.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -86,15 +86,15 @@ require ( go.einride.tech/aip v0.68.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect - golang.org/x/crypto v0.28.0 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/time v0.7.0 // indirect - google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/time v0.8.0 // indirect + google.golang.org/genproto v0.0.0-20241113202542-65e8d215514f // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect modernc.org/libc v1.55.3 // indirect diff --git a/go.sum b/go.sum index b1755c8db..0b0edc75f 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,18 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= -cloud.google.com/go/auth v0.9.9 h1:BmtbpNQozo8ZwW2t7QJjnrQtdganSdmqeIBxHxNkEZQ= -cloud.google.com/go/auth v0.9.9/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= -cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= -cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= +cloud.google.com/go/auth v0.10.2 h1:oKF7rgBfSHdp/kuhXtqU/tNDr0mZqhYbEh+6SiqzkKo= +cloud.google.com/go/auth v0.10.2/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth/oauth2adapt v0.2.5 h1:2p29+dePqsCHPP1bqDJcKj4qxRyYCcbzKpFyKGt3MTk= +cloud.google.com/go/auth/oauth2adapt v0.2.5/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= -cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU= -cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g= -cloud.google.com/go/kms v1.20.0 h1:uKUvjGqbBlI96xGE669hcVnEMw1Px/Mvfa62dhM5UrY= -cloud.google.com/go/kms v1.20.0/go.mod h1:/dMbFF1tLLFnQV44AoI2GlotbjowyUfgVwezxW291fM= -cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= -cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= +cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA= +cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= +cloud.google.com/go/kms v1.20.1 h1:og29Wv59uf2FVaZlesaiDAqHFzHaoUyHI3HYp9VUHVg= +cloud.google.com/go/kms v1.20.1/go.mod h1:LywpNiVCvzYNJWS9JUcGJSVTNSwPwi0vBAotzDqn2nc= +cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc= +cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= cloud.google.com/go/pubsub v1.45.1 h1:ZC/UzYcrmK12THWn1P72z+Pnp2vu/zCZRXyhAfP1hJY= cloud.google.com/go/pubsub v1.45.1/go.mod h1:3bn7fTmzZFwaUjllitv1WlsNMkqBgGUb3UdMhI54eCc= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= @@ -107,16 +107,16 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= -github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= -github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= +github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o= +github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/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.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -197,8 +197,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -218,26 +218,26 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.5 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 h1:4BZHA+B1wXEQoGNHxW8mURaLhcdGwvRnmhGbm+odRbc= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0/go.mod h1:3qi2EEwMgB4xnKgPLqsDP3j9qxnHDZeHsnAxfjQqTko= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy03iVTXP6ffeN2iXrxfGsZGCjVx0/4KlizjyBwU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ= go.opentelemetry.io/otel/exporters/prometheus v0.52.0 h1:kmU3H0b9ufFSi8IQCcxack+sWUblKkFbqWYs6YiACGQ= go.opentelemetry.io/otel/exporters/prometheus v0.52.0/go.mod h1:+wsAp2+JhuGXX7YRkjlkx6hyWY3ogFPfNA4x3nyiAh0= -go.opentelemetry.io/otel/exporters/zipkin v1.31.0 h1:CgucL0tj3717DJnni7HVVB2wExzi8c2zJNEA2BhLMvI= -go.opentelemetry.io/otel/exporters/zipkin v1.31.0/go.mod h1:rfzOVNiSwIcWtEC2J8epwG26fiaXlYvLySJ7bwsrtAE= -go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= -go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= -go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= -go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/exporters/zipkin v1.32.0 h1:6O8HgLHPXtXE9QEKEWkBImL9mEKCGEl+m+OncVO53go= +go.opentelemetry.io/otel/exporters/zipkin v1.32.0/go.mod h1:+MFvorlowjy0iWnsKaNxC1kzczSxe71mw85h4p8yEvg= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= go.opentelemetry.io/otel/sdk/metric v1.30.0 h1:QJLT8Pe11jyHBHfSAgYH7kEmT24eX792jZO1bo4BXkM= go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -253,8 +253,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -280,8 +280,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= @@ -292,8 +292,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -308,15 +308,15 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -324,10 +324,10 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -345,20 +345,20 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.203.0 h1:SrEeuwU3S11Wlscsn+LA1kb/Y5xT8uggJSkIhD08NAU= -google.golang.org/api v0.203.0/go.mod h1:BuOVyCSYEPwJb3npWvDnNmFI92f3GeRnHNkETneT3SI= +google.golang.org/api v0.209.0 h1:Ja2OXNlyRlWCWu8o+GgI4yUn/wz9h/5ZfFbKz+dQX+w= +google.golang.org/api v0.209.0/go.mod h1:I53S168Yr/PNDNMi5yPnDc0/LGRZO6o7PoEbl/HY3CM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 h1:Df6WuGvthPzc+JiQ/G+m+sNX24kc0aTBqoDN/0yyykE= -google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53/go.mod h1:fheguH3Am2dGp1LfXkrvwqC/KlFq8F0nLq3LryOMrrE= -google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg= -google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto v0.0.0-20241113202542-65e8d215514f h1:zDoHYmMzMacIdjNe+P2XiTmPsLawi/pCbSPfxt6lTfw= +google.golang.org/genproto v0.0.0-20241113202542-65e8d215514f/go.mod h1:Q5m6g8b5KaFFzsQFIGdJkSJDGeJiybVenoYFMMa3ohI= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f h1:C1QccEa9kUwvMgEUORqQD9S17QesQijxjZ84sO82mfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -376,8 +376,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -414,8 +414,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= -modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= +modernc.org/sqlite v1.34.1 h1:u3Yi6M0N8t9yKRDwhXcyp1eS5/ErhPTBggxWFuR6Hfk= +modernc.org/sqlite v1.34.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/pkg/gofr/container/container.go b/pkg/gofr/container/container.go index d7e6cee91..d360b5f1b 100644 --- a/pkg/gofr/container/container.go +++ b/pkg/gofr/container/container.go @@ -58,6 +58,7 @@ type Container struct { Mongo Mongo Solr Solr DGraph Dgraph + OpenTSDB OpenTSDB KVStore KVStore diff --git a/pkg/gofr/container/datasources.go b/pkg/gofr/container/datasources.go index 15bf02ad7..7404afd7e 100644 --- a/pkg/gofr/container/datasources.go +++ b/pkg/gofr/container/datasources.go @@ -395,3 +395,111 @@ type DgraphProvider interface { Dgraph provider } + +type OpenTSDBProvider interface { + OpenTSDB + provider +} + +// OpenTSDB provides methods for GoFr applications to communicate with OpenTSDB +// through its REST APIs. Each method corresponds to an API endpoint defined in the +// OpenTSDB documentation (http://opentsdb.net/docs/build/html/api_http/index.html#api-endpoints). +type OpenTSDB interface { + + // HealthChecker verifies if the OpenTSDB server is reachable. + // Returns an error if the server is unreachable, otherwise nil. + HealthChecker + + // PutDataPoints sends data to the 'POST /api/put' endpoint to store metrics in OpenTSDB. + // + // Parameters: + // - ctx: Context for managing request lifetime. + // - data: A slice of DataPoint objects; must contain at least one entry. + // - queryParam: Specifies the response format: + // - client.PutRespWithSummary: Requests a summary response. + // - client.PutRespWithDetails: Requests detailed response information. + // - Empty string (""): No additional response details. + // - res: A pointer to PutResponse, where the server's response will be stored. + // + // Returns: + // - Error if parameters are invalid, response parsing fails, or if connectivity issues occur. + PutDataPoints(ctx context.Context, data any, queryParam string, res any) error + + // QueryDataPoints retrieves data using the 'GET /api/query' endpoint based on the specified parameters. + // + // Parameters: + // - ctx: Context for managing request lifetime. + // - param: An instance of QueryParam with query parameters for filtering data. + // - res: A pointer to QueryResponse, where the server's response will be stored. + // + // Returns: + // - Error if parameters are invalid, response parsing fails, or if connectivity issues occur. + QueryDataPoints(ctx context.Context, param any, res any) error + + // QueryLatestDataPoints fetches the latest data point(s) using the 'GET /api/query/last' endpoint, + // supported in OpenTSDB v2.2 and later. + // + // Parameters: + // - ctx: Context for managing request lifetime. + // - param: An instance of QueryLastParam with query parameters for the latest data point. + // - res: A pointer to QueryLastResponse, where the server's response will be stored. + // + // Returns: + // - Error if parameters are invalid, response parsing fails, or if connectivity issues occur. + QueryLatestDataPoints(ctx context.Context, param any, res any) error + + // GetAggregators retrieves available aggregation functions using the 'GET /api/aggregators' endpoint. + // + // Parameters: + // - ctx: Context for managing request lifetime. + // - res: A pointer to AggregatorsResponse, where the server's response will be stored. + // + // Returns: + // - Error if response parsing fails or if connectivity issues occur. + GetAggregators(ctx context.Context, res any) error + + // QueryAnnotation retrieves a single annotation from OpenTSDB using the 'GET /api/annotation' endpoint. + // + // Parameters: + // - ctx: Context for managing request lifetime. + // - queryAnnoParam: A map of parameters for the annotation query, such as client.AnQueryStartTime, client.AnQueryTSUid. + // - res: A pointer to AnnotationResponse, where the server's response will be stored. + // + // Returns: + // - Error if parameters are invalid, response parsing fails, or if connectivity issues occur. + QueryAnnotation(ctx context.Context, queryAnnoParam map[string]any, res any) error + + // PostAnnotation creates or updates an annotation in OpenTSDB using the 'POST /api/annotation' endpoint. + // + // Parameters: + // - ctx: Context for managing request lifetime. + // - annotation: The annotation to be created or updated. + // - res: A pointer to AnnotationResponse, where the server's response will be stored. + // + // Returns: + // - Error if parameters are invalid, response parsing fails, or if connectivity issues occur. + PostAnnotation(ctx context.Context, annotation any, res any) error + + // PutAnnotation creates or replaces an annotation in OpenTSDB using the 'PUT /api/annotation' endpoint. + // Fields not included in the request will be reset to default values. + // + // Parameters: + // - ctx: Context for managing request lifetime. + // - annotation: The annotation to be created or replaced. + // - res: A pointer to AnnotationResponse, where the server's response will be stored. + // + // Returns: + // - Error if parameters are invalid, response parsing fails, or if connectivity issues occur. + PutAnnotation(ctx context.Context, annotation any, res any) error + + // DeleteAnnotation removes an annotation from OpenTSDB using the 'DELETE /api/annotation' endpoint. + // + // Parameters: + // - ctx: Context for managing request lifetime. + // - annotation: The annotation to be deleted. + // - res: A pointer to AnnotationResponse, where the server's response will be stored. + // + // Returns: + // - Error if parameters are invalid, response parsing fails, or if connectivity issues occur. + DeleteAnnotation(ctx context.Context, annotation any, res any) error +} diff --git a/pkg/gofr/container/health.go b/pkg/gofr/container/health.go index dbed41f5a..85b33ac68 100644 --- a/pkg/gofr/container/health.go +++ b/pkg/gofr/container/health.go @@ -65,6 +65,7 @@ func checkExternalDBHealth(ctx context.Context, c *Container, healthMap map[stri "clickHouse": c.Clickhouse, "kv-store": c.KVStore, "dgraph": c.DGraph, + "opentsdb": c.OpenTSDB, } for name, service := range services { diff --git a/pkg/gofr/container/health_test.go b/pkg/gofr/container/health_test.go index 34ba0bda1..de4947fa8 100644 --- a/pkg/gofr/container/health_test.go +++ b/pkg/gofr/container/health_test.go @@ -79,8 +79,14 @@ func TestContainer_Health(t *testing.T) { "error": "dgraph not connected", }, }, + "opentsdb": datasource.Health{ + Status: tc.datasourceHealth, Details: map[string]any{ + "host": "localhost:8000", + "error": "opentsdb not connected", + }, + }, "test-service": &service.Health{ - Status: "UP", Details: map[string]interface{}{ + Status: "UP", Details: map[string]any{ "host": strings.TrimPrefix(srv.URL, "http://"), }, }, @@ -168,4 +174,12 @@ func registerMocks(mocks *Mocks, health string) { "error": "dgraph not connected", }, }, nil) + + mocks.OpenTSDB.EXPECT().HealthCheck(context.Background()).Return(datasource.Health{ + Status: health, + Details: map[string]any{ + "host": "localhost:8000", + "error": "opentsdb not connected", + }, + }, nil) } diff --git a/pkg/gofr/container/mock_container.go b/pkg/gofr/container/mock_container.go index 7327656ea..02959c6d4 100644 --- a/pkg/gofr/container/mock_container.go +++ b/pkg/gofr/container/mock_container.go @@ -23,6 +23,7 @@ type Mocks struct { Mongo *MockMongo KVStore *MockKVStore DGraph *MockDgraph + OpenTSDB *MockOpenTSDBProvider File *file.MockFileSystemProvider HTTPService *service.MockHTTP Metrics *MockMetrics @@ -82,6 +83,9 @@ func NewMockContainer(t *testing.T, options ...options) (*Container, *Mocks) { dgraphMock := NewMockDgraph(ctrl) container.DGraph = dgraphMock + opentsdbMock := NewMockOpenTSDBProvider(ctrl) + container.OpenTSDB = opentsdbMock + var httpMock *service.MockHTTP container.Services = make(map[string]service.HTTP) @@ -110,6 +114,7 @@ func NewMockContainer(t *testing.T, options ...options) (*Container, *Mocks) { File: fileStoreMock, HTTPService: httpMock, DGraph: dgraphMock, + OpenTSDB: opentsdbMock, Metrics: mockMetrics, } diff --git a/pkg/gofr/container/mock_datasources.go b/pkg/gofr/container/mock_datasources.go index e88e028ac..0b9676e5b 100644 --- a/pkg/gofr/container/mock_datasources.go +++ b/pkg/gofr/container/mock_datasources.go @@ -10906,3 +10906,351 @@ func (mr *MockDgraphProviderMockRecorder) UseTracer(tracer any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockDgraphProvider)(nil).UseTracer), tracer) } + +// MockOpenTSDBProvider is a mock of OpenTSDBProvider interface. +type MockOpenTSDBProvider struct { + ctrl *gomock.Controller + recorder *MockOpenTSDBProviderMockRecorder +} + +// MockOpenTSDBProviderMockRecorder is the mock recorder for MockOpenTSDBProvider. +type MockOpenTSDBProviderMockRecorder struct { + mock *MockOpenTSDBProvider +} + +// NewMockOpenTSDBProvider creates a new mock instance. +func NewMockOpenTSDBProvider(ctrl *gomock.Controller) *MockOpenTSDBProvider { + mock := &MockOpenTSDBProvider{ctrl: ctrl} + mock.recorder = &MockOpenTSDBProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOpenTSDBProvider) EXPECT() *MockOpenTSDBProviderMockRecorder { + return m.recorder +} + +// Connect mocks base method. +func (m *MockOpenTSDBProvider) Connect() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Connect") +} + +// Connect indicates an expected call of Connect. +func (mr *MockOpenTSDBProviderMockRecorder) Connect() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockOpenTSDBProvider)(nil).Connect)) +} + +// DeleteAnnotation mocks base method. +func (m *MockOpenTSDBProvider) DeleteAnnotation(ctx context.Context, annotation, res any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteAnnotation", ctx, annotation, res) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteAnnotation indicates an expected call of DeleteAnnotation. +func (mr *MockOpenTSDBProviderMockRecorder) DeleteAnnotation(ctx, annotation, res any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAnnotation", reflect.TypeOf((*MockOpenTSDBProvider)(nil).DeleteAnnotation), ctx, annotation, res) +} + +// GetAggregators mocks base method. +func (m *MockOpenTSDBProvider) GetAggregators(ctx context.Context, res any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAggregators", ctx, res) + ret0, _ := ret[0].(error) + return ret0 +} + +// GetAggregators indicates an expected call of GetAggregators. +func (mr *MockOpenTSDBProviderMockRecorder) GetAggregators(ctx, res any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAggregators", reflect.TypeOf((*MockOpenTSDBProvider)(nil).GetAggregators), ctx, res) +} + +// HealthCheck mocks base method. +func (m *MockOpenTSDBProvider) HealthCheck(arg0 context.Context) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HealthCheck", arg0) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HealthCheck indicates an expected call of HealthCheck. +func (mr *MockOpenTSDBProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockOpenTSDBProvider)(nil).HealthCheck), arg0) +} + +// PostAnnotation mocks base method. +func (m *MockOpenTSDBProvider) PostAnnotation(ctx context.Context, annotation, res any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PostAnnotation", ctx, annotation, res) + ret0, _ := ret[0].(error) + return ret0 +} + +// PostAnnotation indicates an expected call of PostAnnotation. +func (mr *MockOpenTSDBProviderMockRecorder) PostAnnotation(ctx, annotation, res any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostAnnotation", reflect.TypeOf((*MockOpenTSDBProvider)(nil).PostAnnotation), ctx, annotation, res) +} + +// PutAnnotation mocks base method. +func (m *MockOpenTSDBProvider) PutAnnotation(ctx context.Context, annotation, res any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PutAnnotation", ctx, annotation, res) + ret0, _ := ret[0].(error) + return ret0 +} + +// PutAnnotation indicates an expected call of PutAnnotation. +func (mr *MockOpenTSDBProviderMockRecorder) PutAnnotation(ctx, annotation, res any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutAnnotation", reflect.TypeOf((*MockOpenTSDBProvider)(nil).PutAnnotation), ctx, annotation, res) +} + +// PutDataPoints mocks base method. +func (m *MockOpenTSDBProvider) PutDataPoints(ctx context.Context, data any, queryParam string, res any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PutDataPoints", ctx, data, queryParam, res) + ret0, _ := ret[0].(error) + return ret0 +} + +// PutDataPoints indicates an expected call of PutDataPoints. +func (mr *MockOpenTSDBProviderMockRecorder) PutDataPoints(ctx, data, queryParam, res any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutDataPoints", reflect.TypeOf((*MockOpenTSDBProvider)(nil).PutDataPoints), ctx, data, queryParam, res) +} + +// QueryAnnotation mocks base method. +func (m *MockOpenTSDBProvider) QueryAnnotation(ctx context.Context, queryAnnoParam map[string]any, res any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryAnnotation", ctx, queryAnnoParam, res) + ret0, _ := ret[0].(error) + return ret0 +} + +// QueryAnnotation indicates an expected call of QueryAnnotation. +func (mr *MockOpenTSDBProviderMockRecorder) QueryAnnotation(ctx, queryAnnoParam, res any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAnnotation", reflect.TypeOf((*MockOpenTSDBProvider)(nil).QueryAnnotation), ctx, queryAnnoParam, res) +} + +// QueryDataPoints mocks base method. +func (m *MockOpenTSDBProvider) QueryDataPoints(ctx context.Context, param, res any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryDataPoints", ctx, param, res) + ret0, _ := ret[0].(error) + return ret0 +} + +// QueryDataPoints indicates an expected call of QueryDataPoints. +func (mr *MockOpenTSDBProviderMockRecorder) QueryDataPoints(ctx, param, res any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryDataPoints", reflect.TypeOf((*MockOpenTSDBProvider)(nil).QueryDataPoints), ctx, param, res) +} + +// QueryLatestDataPoints mocks base method. +func (m *MockOpenTSDBProvider) QueryLatestDataPoints(ctx context.Context, param, res any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryLatestDataPoints", ctx, param, res) + ret0, _ := ret[0].(error) + return ret0 +} + +// QueryLatestDataPoints indicates an expected call of QueryLatestDataPoints. +func (mr *MockOpenTSDBProviderMockRecorder) QueryLatestDataPoints(ctx, param, res any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryLatestDataPoints", reflect.TypeOf((*MockOpenTSDBProvider)(nil).QueryLatestDataPoints), ctx, param, res) +} + +// UseLogger mocks base method. +func (m *MockOpenTSDBProvider) UseLogger(logger any) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UseLogger", logger) +} + +// UseLogger indicates an expected call of UseLogger. +func (mr *MockOpenTSDBProviderMockRecorder) UseLogger(logger any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockOpenTSDBProvider)(nil).UseLogger), logger) +} + +// UseMetrics mocks base method. +func (m *MockOpenTSDBProvider) UseMetrics(metrics any) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UseMetrics", metrics) +} + +// UseMetrics indicates an expected call of UseMetrics. +func (mr *MockOpenTSDBProviderMockRecorder) UseMetrics(metrics any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockOpenTSDBProvider)(nil).UseMetrics), metrics) +} + +// UseTracer mocks base method. +func (m *MockOpenTSDBProvider) UseTracer(tracer any) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UseTracer", tracer) +} + +// UseTracer indicates an expected call of UseTracer. +func (mr *MockOpenTSDBProviderMockRecorder) UseTracer(tracer any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockOpenTSDBProvider)(nil).UseTracer), tracer) +} + +// MockOpenTSDB is a mock of OpenTSDB interface. +type MockOpenTSDB struct { + ctrl *gomock.Controller + recorder *MockOpenTSDBMockRecorder +} + +// MockOpenTSDBMockRecorder is the mock recorder for MockOpenTSDB. +type MockOpenTSDBMockRecorder struct { + mock *MockOpenTSDB +} + +// NewMockOpenTSDB creates a new mock instance. +func NewMockOpenTSDB(ctrl *gomock.Controller) *MockOpenTSDB { + mock := &MockOpenTSDB{ctrl: ctrl} + mock.recorder = &MockOpenTSDBMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOpenTSDB) EXPECT() *MockOpenTSDBMockRecorder { + return m.recorder +} + +// DeleteAnnotation mocks base method. +func (m *MockOpenTSDB) DeleteAnnotation(ctx context.Context, annotation, res any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteAnnotation", ctx, annotation, res) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteAnnotation indicates an expected call of DeleteAnnotation. +func (mr *MockOpenTSDBMockRecorder) DeleteAnnotation(ctx, annotation, res any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAnnotation", reflect.TypeOf((*MockOpenTSDB)(nil).DeleteAnnotation), ctx, annotation, res) +} + +// GetAggregators mocks base method. +func (m *MockOpenTSDB) GetAggregators(ctx context.Context, res any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAggregators", ctx, res) + ret0, _ := ret[0].(error) + return ret0 +} + +// GetAggregators indicates an expected call of GetAggregators. +func (mr *MockOpenTSDBMockRecorder) GetAggregators(ctx, res any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAggregators", reflect.TypeOf((*MockOpenTSDB)(nil).GetAggregators), ctx, res) +} + +// HealthCheck mocks base method. +func (m *MockOpenTSDB) HealthCheck(arg0 context.Context) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HealthCheck", arg0) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HealthCheck indicates an expected call of HealthCheck. +func (mr *MockOpenTSDBMockRecorder) HealthCheck(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockOpenTSDB)(nil).HealthCheck), arg0) +} + +// PostAnnotation mocks base method. +func (m *MockOpenTSDB) PostAnnotation(ctx context.Context, annotation, res any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PostAnnotation", ctx, annotation, res) + ret0, _ := ret[0].(error) + return ret0 +} + +// PostAnnotation indicates an expected call of PostAnnotation. +func (mr *MockOpenTSDBMockRecorder) PostAnnotation(ctx, annotation, res any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostAnnotation", reflect.TypeOf((*MockOpenTSDB)(nil).PostAnnotation), ctx, annotation, res) +} + +// PutAnnotation mocks base method. +func (m *MockOpenTSDB) PutAnnotation(ctx context.Context, annotation, res any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PutAnnotation", ctx, annotation, res) + ret0, _ := ret[0].(error) + return ret0 +} + +// PutAnnotation indicates an expected call of PutAnnotation. +func (mr *MockOpenTSDBMockRecorder) PutAnnotation(ctx, annotation, res any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutAnnotation", reflect.TypeOf((*MockOpenTSDB)(nil).PutAnnotation), ctx, annotation, res) +} + +// PutDataPoints mocks base method. +func (m *MockOpenTSDB) PutDataPoints(ctx context.Context, data any, queryParam string, res any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PutDataPoints", ctx, data, queryParam, res) + ret0, _ := ret[0].(error) + return ret0 +} + +// PutDataPoints indicates an expected call of PutDataPoints. +func (mr *MockOpenTSDBMockRecorder) PutDataPoints(ctx, data, queryParam, res any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutDataPoints", reflect.TypeOf((*MockOpenTSDB)(nil).PutDataPoints), ctx, data, queryParam, res) +} + +// QueryAnnotation mocks base method. +func (m *MockOpenTSDB) QueryAnnotation(ctx context.Context, queryAnnoParam map[string]any, res any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryAnnotation", ctx, queryAnnoParam, res) + ret0, _ := ret[0].(error) + return ret0 +} + +// QueryAnnotation indicates an expected call of QueryAnnotation. +func (mr *MockOpenTSDBMockRecorder) QueryAnnotation(ctx, queryAnnoParam, res any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAnnotation", reflect.TypeOf((*MockOpenTSDB)(nil).QueryAnnotation), ctx, queryAnnoParam, res) +} + +// QueryDataPoints mocks base method. +func (m *MockOpenTSDB) QueryDataPoints(ctx context.Context, param, res any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryDataPoints", ctx, param, res) + ret0, _ := ret[0].(error) + return ret0 +} + +// QueryDataPoints indicates an expected call of QueryDataPoints. +func (mr *MockOpenTSDBMockRecorder) QueryDataPoints(ctx, param, res any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryDataPoints", reflect.TypeOf((*MockOpenTSDB)(nil).QueryDataPoints), ctx, param, res) +} + +// QueryLatestDataPoints mocks base method. +func (m *MockOpenTSDB) QueryLatestDataPoints(ctx context.Context, param, res any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryLatestDataPoints", ctx, param, res) + ret0, _ := ret[0].(error) + return ret0 +} + +// QueryLatestDataPoints indicates an expected call of QueryLatestDataPoints. +func (mr *MockOpenTSDBMockRecorder) QueryLatestDataPoints(ctx, param, res any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryLatestDataPoints", reflect.TypeOf((*MockOpenTSDB)(nil).QueryLatestDataPoints), ctx, param, res) +} diff --git a/pkg/gofr/datasource/README.md b/pkg/gofr/datasource/README.md index aa34b8602..667ff903a 100644 --- a/pkg/gofr/datasource/README.md +++ b/pkg/gofr/datasource/README.md @@ -70,19 +70,20 @@ Therefore, GoFr utilizes a pluggable approach for new datasources by separating ## Supported Datasources | Datasource | Health-Check | Logs | Metrics | Traces | As Driver | -|----------------|-----------|------|-------|--------|-----------| -| MySQL | ✅ | ✅ | ✅ | ✅ | | -| REDIS | ✅ | ✅ | ✅ | ✅ | | -| PostgreSQL | ✅ | ✅ | ✅ | ✅ | | -| MongoDB | ✅ | ✅ | ✅ | ✅ | ✅ | -| SQLite | ✅ | ✅ | ✅ | ✅ | | -| BadgerDB | ✅ | ✅ | ✅ | ✅ | ✅ | -| Cassandra | ✅ | ✅ | ✅ | ✅ | ✅ | -| Clickhouse | | ✅ | ✅ | ✅ | ✅ | -| FTP | | ✅ | | | ✅ | -| SFTP | | ✅ | | | ✅ | -| Solr | | ✅ | ✅ | ✅ | ✅ | +|----------------|-----------|------|------|--------|-----------| +| MySQL | ✅ | ✅ | ✅ | ✅ | | +| REDIS | ✅ | ✅ | ✅ | ✅ | | +| PostgreSQL | ✅ | ✅ | ✅ | ✅ | | +| MongoDB | ✅ | ✅ | ✅ | ✅ | ✅ | +| SQLite | ✅ | ✅ | ✅ | ✅ | | +| BadgerDB | ✅ | ✅ | ✅ | ✅ | ✅ | +| Cassandra | ✅ | ✅ | ✅ | ✅ | ✅ | +| Clickhouse | | ✅ | ✅ | ✅ | ✅ | +| FTP | | ✅ | | | ✅ | +| SFTP | | ✅ | | | ✅ | +| Solr | | ✅ | ✅ | ✅ | ✅ | | DGraph | ✅ | ✅ |✅ | ✅ || | Azure Eventhub | | ✅ |✅ | |✅| +| OpenTSDB | ✅ | ✅ | | ✅ | ✅ | diff --git a/pkg/gofr/datasource/cassandra/cassandra.go b/pkg/gofr/datasource/cassandra/cassandra.go index 76d1fa93f..2490976fa 100644 --- a/pkg/gofr/datasource/cassandra/cassandra.go +++ b/pkg/gofr/datasource/cassandra/cassandra.go @@ -62,11 +62,11 @@ func New(conf Config) *Client { // Connect establishes a connection to Cassandra and registers metrics using the provided configuration when the client was Created. func (c *Client) Connect() { - c.logger.Logf("connecting to cassandra at %v on port %v to keyspace %v", c.config.Hosts, c.config.Port, c.config.Keyspace) + c.logger.Debugf("connecting to Cassandra at %v on port %v to keyspace %v", c.config.Hosts, c.config.Port, c.config.Keyspace) sess, err := c.cassandra.clusterConfig.createSession() if err != nil { - c.logger.Error("error connecting to cassandra: ", err) + c.logger.Error("error connecting to Cassandra: ", err) return } @@ -328,7 +328,7 @@ func (*Client) getColumnsFromColumnsInfo(columns []gocql.ColumnInfo) []string { } func (c *Client) sendOperationStats(ql *QueryLog, startTime time.Time, method string, span trace.Span) { - duration := time.Since(startTime).Milliseconds() + duration := time.Since(startTime).Microseconds() ql.Duration = duration diff --git a/pkg/gofr/datasource/cassandra/cassandra_test.go b/pkg/gofr/datasource/cassandra/cassandra_test.go index 01e8b2201..0ebfaab83 100644 --- a/pkg/gofr/datasource/cassandra/cassandra_test.go +++ b/pkg/gofr/datasource/cassandra/cassandra_test.go @@ -1,5 +1,3 @@ -//go:build exclude - package cassandra import ( @@ -82,7 +80,7 @@ func Test_Connect(t *testing.T) { cassandraBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10} - mockLogger.EXPECT().Logf("connecting to cassandra at %v on port %v to keyspace %v", "host1", 9042, "test_keyspace") + mockLogger.EXPECT().Debugf("connecting to Cassandra at %v on port %v to keyspace %v", "host1", 9042, "test_keyspace") testCases := []struct { desc string @@ -93,12 +91,12 @@ func Test_Connect(t *testing.T) { mockClusterConfig.EXPECT().createSession().Return(&cassandraSession{}, nil).Times(1) mockMetrics.EXPECT().NewHistogram("app_cassandra_stats", "Response time of CASSANDRA queries in milliseconds.", cassandraBuckets).Times(1) - mockLogger.EXPECT().Logf("connecting to cassandra at %v on port %v to keyspace %v", "host1", 9042, "test_keyspace") + mockLogger.EXPECT().Debugf("connecting to Cassandra at %v on port %v to keyspace %v", "host1", 9042, "test_keyspace") mockLogger.EXPECT().Logf("connected to '%s' keyspace at host '%s' and port '%d'", "test_keyspace", "host1", 9042) }, &cassandraSession{}}, {"connection failure", func() { mockClusterConfig.EXPECT().createSession().Return(nil, errConnFail).Times(1) - mockLogger.EXPECT().Error("error connecting to cassandra: ") + mockLogger.EXPECT().Error("error connecting to Cassandra: ") }, nil}, } diff --git a/pkg/gofr/datasource/cassandra/mock_interfaces.go b/pkg/gofr/datasource/cassandra/mock_interfaces.go index 63ebd66eb..0bbee511f 100644 --- a/pkg/gofr/datasource/cassandra/mock_interfaces.go +++ b/pkg/gofr/datasource/cassandra/mock_interfaces.go @@ -1,5 +1,3 @@ -//go:build exclude - // Code generated by MockGen. DO NOT EDIT. // Source: interfaces.go // diff --git a/pkg/gofr/datasource/cassandra/mock_logger.go b/pkg/gofr/datasource/cassandra/mock_logger.go index c29a7b4b3..d3f839f3e 100644 --- a/pkg/gofr/datasource/cassandra/mock_logger.go +++ b/pkg/gofr/datasource/cassandra/mock_logger.go @@ -1,5 +1,3 @@ -//go:build exclude - // Code generated by MockGen. DO NOT EDIT. // Source: logger.go // diff --git a/pkg/gofr/datasource/cassandra/mock_metrics.go b/pkg/gofr/datasource/cassandra/mock_metrics.go index e1348111c..3f0122510 100644 --- a/pkg/gofr/datasource/cassandra/mock_metrics.go +++ b/pkg/gofr/datasource/cassandra/mock_metrics.go @@ -1,5 +1,3 @@ -//go:build exclude - // Code generated by MockGen. DO NOT EDIT. // Source: metrics.go // diff --git a/pkg/gofr/datasource/clickhouse/clickhouse.go b/pkg/gofr/datasource/clickhouse/clickhouse.go index 3e21221cf..3ae9ee262 100644 --- a/pkg/gofr/datasource/clickhouse/clickhouse.go +++ b/pkg/gofr/datasource/clickhouse/clickhouse.go @@ -70,7 +70,7 @@ func (c *client) UseTracer(tracer any) { func (c *client) Connect() { var err error - c.logger.Logf("connecting to clickhouse db at %v to database %v", c.config.Hosts, c.config.Database) + c.logger.Debugf("connecting to Clickhouse db at %v to database %v", c.config.Hosts, c.config.Database) clickHouseBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10} c.metrics.NewHistogram("app_clickhouse_stats", "Response time of Clickhouse queries in milliseconds.", clickHouseBuckets...) @@ -91,7 +91,7 @@ func (c *client) Connect() { }) if err != nil { - c.logger.Errorf("error while connecting to clickhouse %v", err) + c.logger.Errorf("error while connecting to Clickhouse %v", err) return } @@ -99,7 +99,7 @@ func (c *client) Connect() { if err = c.conn.Ping(ctx); err != nil { c.logger.Errorf("ping failed with error %v", err) } else { - c.logger.Logf("successfully connected to clickhouseDB") + c.logger.Logf("successfully connected to ClickhouseDB") } go pushDBMetrics(c.conn, c.metrics) @@ -169,7 +169,7 @@ func (c *client) AsyncInsert(ctx context.Context, query string, wait bool, args func (c *client) sendOperationStats(start time.Time, methodType, query string, method string, span trace.Span, args ...interface{}) { - duration := time.Since(start).Milliseconds() + duration := time.Since(start).Microseconds() c.logger.Debug(&Log{ Type: methodType, diff --git a/pkg/gofr/datasource/clickhouse/clickhouse_test.go b/pkg/gofr/datasource/clickhouse/clickhouse_test.go index 5471eb019..237353bc3 100644 --- a/pkg/gofr/datasource/clickhouse/clickhouse_test.go +++ b/pkg/gofr/datasource/clickhouse/clickhouse_test.go @@ -1,5 +1,3 @@ -//go:build exclude - package clickhouse import ( @@ -54,7 +52,7 @@ func Test_ClickHouse_ConnectAndMetricRegistrationAndPingFailure(t *testing.T) { mockMetric.EXPECT().NewGauge("app_clickhouse_idle_connections", "Number of idle Clickhouse connections.") mockMetric.EXPECT().SetGauge("app_clickhouse_open_connections", gomock.Any()).AnyTimes() mockMetric.EXPECT().SetGauge("app_clickhouse_idle_connections", gomock.Any()).AnyTimes() - mockLogger.EXPECT().Logf("connecting to clickhouse db at %v to database %v", "localhost:8000", "test") + mockLogger.EXPECT().Debugf("connecting to Clickhouse db at %v to database %v", "localhost:8000", "test") mockLogger.EXPECT().Errorf("ping failed with error %v", gomock.Any()) cl.Connect() diff --git a/pkg/gofr/datasource/clickhouse/mock_interface.go b/pkg/gofr/datasource/clickhouse/mock_interface.go index d7344c31b..cec06b107 100644 --- a/pkg/gofr/datasource/clickhouse/mock_interface.go +++ b/pkg/gofr/datasource/clickhouse/mock_interface.go @@ -1,5 +1,3 @@ -//go:build exclude - // Code generated by MockGen. DO NOT EDIT. // Source: interface.go // diff --git a/pkg/gofr/datasource/clickhouse/mock_logger.go b/pkg/gofr/datasource/clickhouse/mock_logger.go index 43eae2c76..bc19666f1 100644 --- a/pkg/gofr/datasource/clickhouse/mock_logger.go +++ b/pkg/gofr/datasource/clickhouse/mock_logger.go @@ -1,13 +1,12 @@ -//go:build exclude - // Code generated by MockGen. DO NOT EDIT. // Source: logger.go // // Generated by this command: // -// mockgen -source=logger.go -destination=mock_logger_old.go -package=clickhouse +// mockgen -source=logger.go -destination=mock_logger.go -package=clickhouse // +// Package clickhouse is a generated GoMock package. package clickhouse import ( diff --git a/pkg/gofr/datasource/clickhouse/mock_metrics.go b/pkg/gofr/datasource/clickhouse/mock_metrics.go index d76e77f0e..ac4746d5d 100644 --- a/pkg/gofr/datasource/clickhouse/mock_metrics.go +++ b/pkg/gofr/datasource/clickhouse/mock_metrics.go @@ -1,5 +1,3 @@ -//go:build exclude - // Code generated by MockGen. DO NOT EDIT. // Source: metrics.go // diff --git a/pkg/gofr/datasource/dgraph/draph.go b/pkg/gofr/datasource/dgraph/dgraph.go similarity index 74% rename from pkg/gofr/datasource/dgraph/draph.go rename to pkg/gofr/datasource/dgraph/dgraph.go index 1e3108e7c..b5d26af39 100644 --- a/pkg/gofr/datasource/dgraph/draph.go +++ b/pkg/gofr/datasource/dgraph/dgraph.go @@ -26,7 +26,6 @@ type Config struct { // Client represents the Dgraph client with logging and metrics. type Client struct { client DgraphClient - conn *grpc.ClientConn logger Logger metrics Metrics config Config @@ -54,16 +53,14 @@ func New(config Config) *Client { // Connect connects to the Dgraph database using the provided configuration. func (d *Client) Connect() { address := fmt.Sprintf("%s:%s", d.config.Host, d.config.Port) - d.logger.Logf("connecting to dgraph at %v", address) + d.logger.Debugf("connecting to Dgraph at %v", address) conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { - d.logger.Errorf("error connecting to Dgraph, err: %v", err) + d.logger.Errorf("error while connecting to Dgraph, err: %v", err) return } - d.logger.Logf("connected to dgraph client at %v:%v", d.config.Host, d.config.Port) - // Register metrics // Register all metrics d.metrics.NewHistogram("dgraph_query_duration", "Response time of Dgraph queries in milliseconds.", @@ -80,9 +77,11 @@ func (d *Client) Connect() { // Check connection by performing a basic health check if _, err := d.HealthCheck(context.Background()); err != nil { - d.logger.Errorf("dgraph health check failed: %v", err) + d.logger.Errorf("error while connecting to Dgraph: %v", err) return } + + d.logger.Logf("connected to Dgraph client at %v:%v", d.config.Host, d.config.Port) } // UseLogger sets the logger for the Dgraph client which asserts the Logger interface. @@ -100,9 +99,9 @@ func (d *Client) UseMetrics(metrics any) { } // UseTracer sets the tracer for DGraph client. -func (c *Client) UseTracer(tracer any) { +func (d *Client) UseTracer(tracer any) { if tracer, ok := tracer.(trace.Tracer); ok { - c.tracer = tracer + d.tracer = tracer } } @@ -110,11 +109,10 @@ func (c *Client) UseTracer(tracer any) { func (d *Client) Query(ctx context.Context, query string) (any, error) { start := time.Now() - ctx, span := d.tracer.Start(ctx, "dgraph-query") - defer span.End() + tracedCtx, span := d.addTrace(ctx, "query") // Execute query - resp, err := d.client.NewTxn().Query(ctx, query) + resp, err := d.client.NewTxn().Query(tracedCtx, query) duration := time.Since(start).Microseconds() // Create and log the query details @@ -124,18 +122,14 @@ func (d *Client) Query(ctx context.Context, query string) (any, error) { Duration: duration, } - span.SetAttributes( - attribute.String("dgraph.query.query", query), - attribute.Int64("dgraph.query.duration", duration), - ) - if err != nil { d.logger.Error("dgraph query failed: ", err) ql.PrettyPrint(d.logger) + return nil, err } - d.sendOperationStats(ctx, ql, "dgraph_query_duration") + d.sendOperationStats(tracedCtx, start, query, "query", span, ql, "dgraph_query_duration") return resp, nil } @@ -145,11 +139,10 @@ func (d *Client) Query(ctx context.Context, query string) (any, error) { func (d *Client) QueryWithVars(ctx context.Context, query string, vars map[string]string) (any, error) { start := time.Now() - ctx, span := d.tracer.Start(ctx, "dgraph-query-with-vars") - defer span.End() + tracedCtx, span := d.addTrace(ctx, "query-with-vars") // Execute the query with variables - resp, err := d.client.NewTxn().QueryWithVars(ctx, query, vars) + resp, err := d.client.NewTxn().QueryWithVars(tracedCtx, query, vars) duration := time.Since(start).Microseconds() // Create and log the query details @@ -159,19 +152,18 @@ func (d *Client) QueryWithVars(ctx context.Context, query string, vars map[strin Duration: duration, } - span.SetAttributes( - attribute.String("dgraph.query.query", query), - attribute.String("dgraph.query.vars", fmt.Sprintf("%v", vars)), - attribute.Int64("dgraph.query.duration", duration), - ) + if span != nil { + span.SetAttributes(attribute.String("dgraph.query.vars", fmt.Sprintf("%v", vars))) + } if err != nil { d.logger.Error("dgraph queryWithVars failed: ", err) ql.PrettyPrint(d.logger) + return nil, err } - d.sendOperationStats(ctx, ql, "dgraph_query_with_vars_duration") + d.sendOperationStats(tracedCtx, start, query, "query-with-vars", span, ql, "dgraph_query_with_vars_duration") return resp, nil } @@ -180,8 +172,7 @@ func (d *Client) QueryWithVars(ctx context.Context, query string, vars map[strin func (d *Client) Mutate(ctx context.Context, mu any) (any, error) { start := time.Now() - ctx, span := d.tracer.Start(ctx, "dgraph-mutate") - defer span.End() + tracedCtx, span := d.addTrace(ctx, "mutate") // Cast to proper mutation type mutation, ok := mu.(*api.Mutation) @@ -190,7 +181,7 @@ func (d *Client) Mutate(ctx context.Context, mu any) (any, error) { } // Execute mutation - resp, err := d.client.NewTxn().Mutate(ctx, mutation) + resp, err := d.client.NewTxn().Mutate(tracedCtx, mutation) duration := time.Since(start).Microseconds() // Create and log the mutation details @@ -200,18 +191,14 @@ func (d *Client) Mutate(ctx context.Context, mu any) (any, error) { Duration: duration, } - span.SetAttributes( - attribute.String("dgraph.mutation.query", mutationToString(mutation)), - attribute.Int64("dgraph.mutation.duration", duration), - ) - if err != nil { d.logger.Error("dgraph mutation failed: ", err) ql.PrettyPrint(d.logger) + return nil, err } - d.sendOperationStats(ctx, ql, "dgraph_mutate_duration") + d.sendOperationStats(tracedCtx, start, mutationToString(mutation), "mutate", span, ql, "dgraph_mutate_duration") return resp, nil } @@ -220,8 +207,7 @@ func (d *Client) Mutate(ctx context.Context, mu any) (any, error) { func (d *Client) Alter(ctx context.Context, op any) error { start := time.Now() - ctx, span := d.tracer.Start(ctx, "dgraph-alter") - defer span.End() + tracedCtx, span := d.addTrace(ctx, "alter") // Cast to proper operation type operation, ok := op.(*api.Operation) @@ -231,7 +217,7 @@ func (d *Client) Alter(ctx context.Context, op any) error { } // Apply the schema changes - err := d.client.Alter(ctx, operation) + err := d.client.Alter(tracedCtx, operation) duration := time.Since(start).Microseconds() // Create and log the operation details @@ -241,18 +227,18 @@ func (d *Client) Alter(ctx context.Context, op any) error { Duration: duration, } - span.SetAttributes( - attribute.String("dgraph.alter.operation", operation.String()), - attribute.Int64("dgraph.alter.duration", duration), - ) + if span != nil { + span.SetAttributes(attribute.String("dgraph.alter.operation", operation.String())) + } if err != nil { d.logger.Error("dgraph alter failed: ", err) ql.PrettyPrint(d.logger) + return err } - d.sendOperationStats(ctx, ql, "dgraph_alter_duration") + d.sendOperationStats(tracedCtx, start, operation.String(), "alter", span, ql, "dgraph_alter_duration") return nil } @@ -283,9 +269,29 @@ func (d *Client) HealthCheck(ctx context.Context) (any, error) { return "UP", nil } -func (d *Client) sendOperationStats(ctx context.Context, query *QueryLog, metricName string) { - query.PrettyPrint(d.logger) - d.metrics.RecordHistogram(ctx, metricName, float64(query.Duration)) +func (d *Client) addTrace(ctx context.Context, method string) (context.Context, trace.Span) { + if d.tracer == nil { + return ctx, nil + } + + tracedCtx, span := d.tracer.Start(ctx, fmt.Sprintf("dgraph-%v", method)) + + return tracedCtx, span +} + +func (d *Client) sendOperationStats(ctx context.Context, start time.Time, query, method string, + span trace.Span, queryLog *QueryLog, metricName string) { + duration := time.Since(start).Microseconds() + + if span != nil { + defer span.End() + + span.SetAttributes(attribute.String(fmt.Sprintf("dgraph.%v.query", method), query)) + span.SetAttributes(attribute.Int64(fmt.Sprintf("dgraph.%v.duration", method), duration)) + } + + queryLog.PrettyPrint(d.logger) + d.metrics.RecordHistogram(ctx, metricName, float64(queryLog.Duration)) } func mutationToString(mutation *api.Mutation) string { @@ -295,5 +301,4 @@ func mutationToString(mutation *api.Mutation) string { } return compacted.String() - } diff --git a/pkg/gofr/datasource/dgraph/dgraph_test.go b/pkg/gofr/datasource/dgraph/dgraph_test.go index ad0b70e92..4b22dd8c0 100644 --- a/pkg/gofr/datasource/dgraph/dgraph_test.go +++ b/pkg/gofr/datasource/dgraph/dgraph_test.go @@ -5,12 +5,16 @@ import ( "errors" "testing" + "go.opentelemetry.io/otel" + "go.uber.org/mock/gomock" + "github.com/dgraph-io/dgo/v210/protos/api" "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" ) func setupDB(t *testing.T) (*Client, *MockDgraphClient, *MockLogger, *MockMetrics) { + t.Helper() + ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -51,7 +55,7 @@ func Test_Query_Success(t *testing.T) { mockTxn.EXPECT().Query(gomock.Any(), "my query").Return(&api.Response{Json: []byte(`{"result": "success"}`)}, nil) - mockLogger.EXPECT().Debug("executing dgraph query") + mockLogger.EXPECT().Debug(gomock.Any()) mockLogger.EXPECT().Debugf("dgraph query succeeded in %dµs", gomock.Any()) mockLogger.EXPECT().Log(gomock.Any()).Times(1) @@ -61,18 +65,20 @@ func Test_Query_Success(t *testing.T) { require.NoError(t, err, "Test_Query_Success Failed!") require.NotNil(t, resp, "Test_Query_Success Failed!") - require.Equal(t, resp, &api.Response{Json: []byte(`{"result": "success"}`)}, "Test_Query_Success Failed!") + require.Equal(t, &api.Response{Json: []byte(`{"result": "success"}`)}, resp, "Test_Query_Success Failed!") } func Test_Query_Error(t *testing.T) { client, mockDgraphClient, mockLogger, _ := setupDB(t) + client.tracer = otel.GetTracerProvider().Tracer("gofr-dgraph") + mockTxn := NewMockTxn(mockDgraphClient.ctrl) mockDgraphClient.EXPECT().NewTxn().Return(mockTxn) mockTxn.EXPECT().Query(gomock.Any(), "my query").Return(nil, errors.New("query failed")) - mockLogger.EXPECT().Debug("executing dgraph query") + mockLogger.EXPECT().Debug(gomock.Any()) mockLogger.EXPECT().Log(gomock.Any()).Times(1) mockLogger.EXPECT().Error("dgraph query failed: ", errors.New("query failed")) @@ -94,7 +100,7 @@ func Test_QueryWithVars_Success(t *testing.T) { mockTxn.EXPECT().QueryWithVars(gomock.Any(), query, vars).Return(&api.Response{Json: []byte(`{"result": "success"}`)}, nil) mockLogger.EXPECT().Debugf("dgraph queryWithVars succeeded in %dµs", gomock.Any()) - mockLogger.EXPECT().Debug("executing dgraph query") + mockLogger.EXPECT().Debug(gomock.Any()) mockLogger.EXPECT().Log(gomock.Any()).Times(1) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "dgraph_query_with_vars_duration", gomock.Any()) @@ -104,7 +110,7 @@ func Test_QueryWithVars_Success(t *testing.T) { require.NoError(t, err, "Test_QueryWithVars_Success Failed!") require.NotNil(t, resp, "Test_QueryWithVars_Success Failed!") - require.Equal(t, resp, &api.Response{Json: []byte(`{"result": "success"}`)}, "Test_QueryWithVars_Success Failed!") + require.Equal(t, &api.Response{Json: []byte(`{"result": "success"}`)}, resp, "Test_QueryWithVars_Success Failed!") } func Test_QueryWithVars_Error(t *testing.T) { @@ -118,6 +124,7 @@ func Test_QueryWithVars_Error(t *testing.T) { mockTxn.EXPECT().QueryWithVars(gomock.Any(), query, vars).Return(nil, errors.New("query failed")) + mockLogger.EXPECT().Debug(gomock.Any()) mockLogger.EXPECT().Error("dgraph queryWithVars failed: ", errors.New("query failed")) mockLogger.EXPECT().Log(gomock.Any()).Times(1) @@ -138,7 +145,7 @@ func Test_Mutate_Success(t *testing.T) { mockTxn.EXPECT().Mutate(gomock.Any(), mutation).Return(&api.Response{Json: []byte(`{"result": "mutation success"}`)}, nil) - mockLogger.EXPECT().Debug("executing dgraph mutation") + mockLogger.EXPECT().Debug(gomock.Any()) mockLogger.EXPECT().Debugf("dgraph mutation succeeded in %dµs", gomock.Any()) mockLogger.EXPECT().Log(gomock.Any()).Times(1) @@ -149,7 +156,7 @@ func Test_Mutate_Success(t *testing.T) { require.NoError(t, err, "Test_Mutate_Success Failed!") require.NotNil(t, resp, "Test_Mutate_Success Failed!") - require.Equal(t, resp, &api.Response{Json: []byte(`{"result": "mutation success"}`)}, "Test_Mutate_Success Failed!") + require.Equal(t, &api.Response{Json: []byte(`{"result": "mutation success"}`)}, resp, "Test_Mutate_Success Failed!") } func Test_Mutate_InvalidMutation(t *testing.T) { @@ -172,7 +179,7 @@ func Test_Mutate_Error(t *testing.T) { mockTxn.EXPECT().Mutate(gomock.Any(), mutation).Return(nil, errors.New("mutation failed")) - mockLogger.EXPECT().Debug("executing dgraph mutation") + mockLogger.EXPECT().Debug(gomock.Any()) mockLogger.EXPECT().Error("dgraph mutation failed: ", errors.New("mutation failed")) mockLogger.EXPECT().Log(gomock.Any()).Times(1) @@ -189,6 +196,7 @@ func Test_Alter_Success(t *testing.T) { op := &api.Operation{} mockDgraphClient.EXPECT().Alter(gomock.Any(), op).Return(nil) + mockLogger.EXPECT().Debug(gomock.Any()) mockLogger.EXPECT().Log(gomock.Any()).Times(1) mockLogger.EXPECT().Debugf("dgraph alter succeeded in %dµs", gomock.Any()) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "dgraph_alter_duration", gomock.Any()) @@ -204,6 +212,7 @@ func Test_Alter_Error(t *testing.T) { op := &api.Operation{} mockDgraphClient.EXPECT().Alter(gomock.Any(), op).Return(errors.New("alter failed")) + mockLogger.EXPECT().Debug(gomock.Any()) mockLogger.EXPECT().Log(gomock.Any()).Times(1) mockLogger.EXPECT().Error("dgraph alter failed: ", errors.New("alter failed")) @@ -216,6 +225,7 @@ func Test_Alter_InvalidOperation(t *testing.T) { client, _, mockLogger, _ := setupDB(t) op := "invalid operation" + mockLogger.EXPECT().Error("invalid operation type provided to alter") err := client.Alter(context.Background(), op) diff --git a/pkg/gofr/datasource/dgraph/logger.go b/pkg/gofr/datasource/dgraph/logger.go index a8d5034e9..ad8efc118 100644 --- a/pkg/gofr/datasource/dgraph/logger.go +++ b/pkg/gofr/datasource/dgraph/logger.go @@ -6,7 +6,7 @@ import ( "strings" ) -// Logger interface with required methods +// Logger interface with required methods. type Logger interface { Debug(args ...interface{}) Debugf(pattern string, args ...interface{}) @@ -16,14 +16,14 @@ type Logger interface { Errorf(pattern string, args ...interface{}) } -// QueryLog represents the structure for query logging +// QueryLog represents the structure for query logging. type QueryLog struct { Type string `json:"type"` URL string `json:"url"` Duration int64 `json:"duration"` // Duration in microseconds } -// PrettyPrint logs the QueryLog in a structured format to the given writer +// PrettyPrint logs the QueryLog in a structured format to the given writer. func (ql *QueryLog) PrettyPrint(logger Logger) { // Format the log string formattedLog := fmt.Sprintf( diff --git a/pkg/gofr/datasource/dgraph/logger_test.go b/pkg/gofr/datasource/dgraph/logger_test.go index e94204d0e..31709f37b 100644 --- a/pkg/gofr/datasource/dgraph/logger_test.go +++ b/pkg/gofr/datasource/dgraph/logger_test.go @@ -15,7 +15,7 @@ func Test_PrettyPrint(t *testing.T) { logger := NewMockLogger(gomock.NewController(t)) - logger.EXPECT().Log(gomock.Any()) + logger.EXPECT().Debug(gomock.Any()) queryLog.PrettyPrint(logger) diff --git a/pkg/gofr/datasource/dgraph/metrics.go b/pkg/gofr/datasource/dgraph/metrics.go index 5373baea6..a8c47a8fe 100644 --- a/pkg/gofr/datasource/dgraph/metrics.go +++ b/pkg/gofr/datasource/dgraph/metrics.go @@ -2,6 +2,7 @@ package dgraph import ( "context" + "github.com/prometheus/client_golang/prometheus" ) @@ -30,7 +31,7 @@ func (p *PrometheusMetrics) NewHistogram(name, desc string, buckets ...float64) } // RecordHistogram records a value to the specified histogram metric with optional labels. -func (p *PrometheusMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) { +func (p *PrometheusMetrics) RecordHistogram(_ context.Context, name string, value float64, labels ...string) { histogram, exists := p.histograms[name] if !exists { // Handle error: histogram not found diff --git a/pkg/gofr/datasource/file/ftp/file.go b/pkg/gofr/datasource/file/ftp/file.go index b88086ff0..22ea3ea85 100644 --- a/pkg/gofr/datasource/file/ftp/file.go +++ b/pkg/gofr/datasource/file/ftp/file.go @@ -389,7 +389,7 @@ func (f *file) WriteAt(p []byte, off int64) (n int, err error) { } func (f *file) sendOperationStats(fl *FileLog, startTime time.Time) { - duration := time.Since(startTime).Milliseconds() + duration := time.Since(startTime).Microseconds() fl.Duration = duration diff --git a/pkg/gofr/datasource/file/ftp/file_test.go b/pkg/gofr/datasource/file/ftp/file_test.go index eb64b3feb..e0952c815 100644 --- a/pkg/gofr/datasource/file/ftp/file_test.go +++ b/pkg/gofr/datasource/file/ftp/file_test.go @@ -1,5 +1,3 @@ -//go:build exclude - package ftp import ( @@ -845,7 +843,7 @@ func runFtpTest(t *testing.T, testFunc func(fs file_interface.FileSystemProvider ftpClient.UseMetrics(mockMetrics) mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes() mockMetrics.EXPECT().NewHistogram(appFtpStats, gomock.Any(), gomock.Any()).AnyTimes() mockMetrics.EXPECT().RecordHistogram(gomock.Any(), appFtpStats, gomock.Any(), "type", gomock.Any(), "status", gomock.Any()).AnyTimes() diff --git a/pkg/gofr/datasource/file/ftp/fs.go b/pkg/gofr/datasource/file/ftp/fs.go index 857464002..28e023df9 100644 --- a/pkg/gofr/datasource/file/ftp/fs.go +++ b/pkg/gofr/datasource/file/ftp/fs.go @@ -90,13 +90,15 @@ func (f *fileSystem) Connect() { Status: &status, }, time.Now()) + f.logger.Debugf("connecting to FTP Server at %v", ftpServer) + if f.config.DialTimeout == 0 { f.config.DialTimeout = time.Second * 5 } conn, err := ftp.Dial(ftpServer, ftp.DialWithTimeout(f.config.DialTimeout)) if err != nil { - f.logger.Errorf("Connection failed: %v", err) + f.logger.Errorf("error while connecting to FTP: %v", err) status = "CONNECTION ERROR" @@ -107,7 +109,7 @@ func (f *fileSystem) Connect() { err = conn.Login(f.config.User, f.config.Password) if err != nil { - f.logger.Errorf("Login failed: %v", err) + f.logger.Errorf("login failed: %v", err) status = "LOGIN ERROR" @@ -116,7 +118,7 @@ func (f *fileSystem) Connect() { status = "LOGIN SUCCESS" - f.logger.Logf("Connected to FTP server at '%v'", ftpServer) + f.logger.Logf("connected to FTP server at '%v'", ftpServer) } // Create creates an empty file on the FTP server. @@ -324,7 +326,7 @@ func (f *fileSystem) Rename(oldname, newname string) error { } func (f *fileSystem) sendOperationStats(fl *FileLog, startTime time.Time) { - duration := time.Since(startTime).Milliseconds() + duration := time.Since(startTime).Microseconds() fl.Duration = duration diff --git a/pkg/gofr/datasource/file/ftp/fs_test.go b/pkg/gofr/datasource/file/ftp/fs_test.go index 1e4252881..8a5012baa 100644 --- a/pkg/gofr/datasource/file/ftp/fs_test.go +++ b/pkg/gofr/datasource/file/ftp/fs_test.go @@ -1,5 +1,3 @@ -//go:build exclude - package ftp import ( diff --git a/pkg/gofr/datasource/file/ftp/go.mod b/pkg/gofr/datasource/file/ftp/go.mod index 6464b1780..28bce10c7 100644 --- a/pkg/gofr/datasource/file/ftp/go.mod +++ b/pkg/gofr/datasource/file/ftp/go.mod @@ -22,4 +22,4 @@ require ( github.com/rogpeppe/go-internal v1.10.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) \ No newline at end of file +) diff --git a/pkg/gofr/datasource/file/ftp/go.sum b/pkg/gofr/datasource/file/ftp/go.sum index 21db9aa6c..10aa2321d 100644 --- a/pkg/gofr/datasource/file/ftp/go.sum +++ b/pkg/gofr/datasource/file/ftp/go.sum @@ -26,15 +26,14 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/gofr/datasource/file/ftp/interface.go b/pkg/gofr/datasource/file/ftp/interface.go index 2407e0413..0b24261fe 100644 --- a/pkg/gofr/datasource/file/ftp/interface.go +++ b/pkg/gofr/datasource/file/ftp/interface.go @@ -11,6 +11,7 @@ import ( // Logger interface is used by ftp package to log information about query execution. type Logger interface { Debug(args ...interface{}) + Debugf(pattern string, args ...interface{}) Logf(pattern string, args ...interface{}) Errorf(pattern string, args ...interface{}) } diff --git a/pkg/gofr/datasource/file/ftp/mock_interface.go b/pkg/gofr/datasource/file/ftp/mock_interface.go index f82c2be0c..e7214d161 100644 --- a/pkg/gofr/datasource/file/ftp/mock_interface.go +++ b/pkg/gofr/datasource/file/ftp/mock_interface.go @@ -1,5 +1,3 @@ -//go:build exclude - // Code generated by MockGen. DO NOT EDIT. // Source: interface.go // @@ -60,6 +58,23 @@ func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...) } +// Debugf mocks base method. +func (m *MockLogger) Debugf(pattern string, args ...any) { + m.ctrl.T.Helper() + varargs := []any{pattern} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debugf", varargs...) +} + +// Debugf indicates an expected call of Debugf. +func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{pattern}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...) +} + // Errorf mocks base method. func (m *MockLogger) Errorf(pattern string, args ...any) { m.ctrl.T.Helper() diff --git a/pkg/gofr/datasource/file/s3/file.go b/pkg/gofr/datasource/file/s3/file.go index ab7dd0e9e..5f6a347cb 100644 --- a/pkg/gofr/datasource/file/s3/file.go +++ b/pkg/gofr/datasource/file/s3/file.go @@ -468,7 +468,7 @@ func (f *s3file) Seek(offset int64, whence int) (int64, error) { // sendOperationStats logs the FileLog of any file operations performed in S3. func (f *s3file) sendOperationStats(fl *FileLog, startTime time.Time) { - duration := time.Since(startTime).Milliseconds() + duration := time.Since(startTime).Microseconds() fl.Duration = duration diff --git a/pkg/gofr/datasource/file/s3/fs.go b/pkg/gofr/datasource/file/s3/fs.go index 84619edf1..d3b0a8eb4 100644 --- a/pkg/gofr/datasource/file/s3/fs.go +++ b/pkg/gofr/datasource/file/s3/fs.go @@ -83,6 +83,8 @@ func (f *fileSystem) Connect() { Message: &msg, }, time.Now()) + f.logger.Debugf("connecting to S3 bucket: %s", f.config.BucketName) + // Load the AWS configuration cfg, err := awsConfig.LoadDefaultConfig(context.TODO(), awsConfig.WithRegion(f.config.Region), @@ -95,6 +97,7 @@ func (f *fileSystem) Connect() { if err != nil { f.logger.Errorf("Failed to load configuration: %v", err) + return } // Create the S3 client from config @@ -109,7 +112,7 @@ func (f *fileSystem) Connect() { st = statusSuccess msg = "S3 Client connected." - f.logger.Logf("Connected to S3 bucket %s", f.config.BucketName) + f.logger.Logf("connected to S3 bucket %s", f.config.BucketName) } // Create creates a new file in the S3 bucket. diff --git a/pkg/gofr/datasource/file/s3/fs_dir.go b/pkg/gofr/datasource/file/s3/fs_dir.go index c6cd33e86..60e13a6c2 100644 --- a/pkg/gofr/datasource/file/s3/fs_dir.go +++ b/pkg/gofr/datasource/file/s3/fs_dir.go @@ -291,7 +291,7 @@ func (f *fileSystem) renameDirectory(st, msg *string, oldPath, newPath string) e // sendOperationStats logs the FileLog of any file operations performed in S3. func (f *fileSystem) sendOperationStats(fl *FileLog, startTime time.Time) { - duration := time.Since(startTime).Milliseconds() + duration := time.Since(startTime).Microseconds() fl.Duration = duration diff --git a/pkg/gofr/datasource/file/s3/fs_test.go b/pkg/gofr/datasource/file/s3/fs_test.go index e68d93d2f..d1b026c6b 100644 --- a/pkg/gofr/datasource/file/s3/fs_test.go +++ b/pkg/gofr/datasource/file/s3/fs_test.go @@ -332,7 +332,7 @@ func runS3Test(t *testing.T, testFunc func(fs file.FileSystemProvider)) { mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes() s3Client := New(&cfg) diff --git a/pkg/gofr/datasource/file/s3/go.mod b/pkg/gofr/datasource/file/s3/go.mod index 47c6e4261..b95d10382 100644 --- a/pkg/gofr/datasource/file/s3/go.mod +++ b/pkg/gofr/datasource/file/s3/go.mod @@ -34,4 +34,4 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) \ No newline at end of file +) diff --git a/pkg/gofr/datasource/file/s3/go.sum b/pkg/gofr/datasource/file/s3/go.sum index 4172b62e1..a8300e171 100644 --- a/pkg/gofr/datasource/file/s3/go.sum +++ b/pkg/gofr/datasource/file/s3/go.sum @@ -44,14 +44,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/gofr/datasource/file/s3/interface.go b/pkg/gofr/datasource/file/s3/interface.go index 0ef5b5983..4b0225f9d 100644 --- a/pkg/gofr/datasource/file/s3/interface.go +++ b/pkg/gofr/datasource/file/s3/interface.go @@ -7,6 +7,7 @@ import ( // Logger interface is used by s3 package to log information about query execution. type Logger interface { Debug(args ...interface{}) + Debugf(pattern string, args ...interface{}) Logf(pattern string, args ...interface{}) Errorf(pattern string, args ...interface{}) } diff --git a/pkg/gofr/datasource/file/s3/mock_interface.go b/pkg/gofr/datasource/file/s3/mock_interface.go index 2fecc7335..87ed34450 100644 --- a/pkg/gofr/datasource/file/s3/mock_interface.go +++ b/pkg/gofr/datasource/file/s3/mock_interface.go @@ -55,6 +55,23 @@ func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...) } +// Debugf mocks base method. +func (m *MockLogger) Debugf(pattern string, args ...any) { + m.ctrl.T.Helper() + varargs := []any{pattern} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debugf", varargs...) +} + +// Debugf indicates an expected call of Debugf. +func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{pattern}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...) +} + // Errorf mocks base method. func (m *MockLogger) Errorf(pattern string, args ...any) { m.ctrl.T.Helper() diff --git a/pkg/gofr/datasource/file/sftp/fs.go b/pkg/gofr/datasource/file/sftp/fs.go index fd39d1baa..3ab5665de 100644 --- a/pkg/gofr/datasource/file/sftp/fs.go +++ b/pkg/gofr/datasource/file/sftp/fs.go @@ -60,11 +60,13 @@ func (f *fileSystem) Connect() { conn, err := ssh.Dial("tcp", addr, config) if err != nil { f.logger.Errorf("failed to connect with sftp with err %v", err) + return } client, err := sftp.NewClient(conn) if err != nil { f.logger.Errorf("failed to create sftp client with err %v", err) + return } f.client = client @@ -255,7 +257,7 @@ func (f *fileSystem) Getwd() (string, error) { } func (f *fileSystem) sendOperationStats(fl *FileLog, startTime time.Time) { - duration := time.Since(startTime).Milliseconds() + duration := time.Since(startTime).Microseconds() fl.Duration = duration diff --git a/pkg/gofr/datasource/file/sftp/go.mod b/pkg/gofr/datasource/file/sftp/go.mod index 7333cae4c..49bb8db63 100644 --- a/pkg/gofr/datasource/file/sftp/go.mod +++ b/pkg/gofr/datasource/file/sftp/go.mod @@ -11,7 +11,7 @@ require ( github.com/stretchr/testify v1.9.0 go.uber.org/mock v0.5.0 gofr.dev v0.19.0 - golang.org/x/crypto v0.28.0 + golang.org/x/crypto v0.29.0 ) require ( @@ -20,6 +20,6 @@ require ( github.com/kr/fs v0.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.26.0 // indirect + golang.org/x/sys v0.27.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) \ No newline at end of file +) diff --git a/pkg/gofr/datasource/file/sftp/go.sum b/pkg/gofr/datasource/file/sftp/go.sum index 5c721e096..7116357d5 100644 --- a/pkg/gofr/datasource/file/sftp/go.sum +++ b/pkg/gofr/datasource/file/sftp/go.sum @@ -18,16 +18,13 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -41,15 +38,13 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -62,4 +57,4 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/gofr/datasource/kv-store/badger/badger.go b/pkg/gofr/datasource/kv-store/badger/badger.go index e8c4e93b0..80a04520d 100644 --- a/pkg/gofr/datasource/kv-store/badger/badger.go +++ b/pkg/gofr/datasource/kv-store/badger/badger.go @@ -54,7 +54,7 @@ func (c *client) UseTracer(tracer any) { // Connect establishes a connection to BadgerDB and registers metrics using the provided configuration when the client was Created. func (c *client) Connect() { - c.logger.Infof("connecting to BadgerDB at %v", c.configs.DirPath) + c.logger.Debugf("connecting to BadgerDB at %v", c.configs.DirPath) badgerBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10} c.metrics.NewHistogram("app_badger_stats", "Response time of Badger queries in milliseconds.", badgerBuckets...) @@ -62,9 +62,12 @@ func (c *client) Connect() { db, err := badger.Open(badger.DefaultOptions(c.configs.DirPath)) if err != nil { c.logger.Errorf("error while connecting to BadgerDB: %v", err) + return } c.db = db + + c.logger.Infof("connected to BadgerDB at %v", c.configs.DirPath) } func (c *client) Get(ctx context.Context, key string) (string, error) { @@ -145,7 +148,7 @@ func (c *client) useTransaction(f func(txn *badger.Txn) error) error { func (c *client) sendOperationStats(start time.Time, methodType string, method string, span trace.Span, kv ...string) { - duration := time.Since(start).Milliseconds() + duration := time.Since(start).Microseconds() c.logger.Debug(&Log{ Type: methodType, diff --git a/pkg/gofr/datasource/kv-store/badger/mock_logger.go b/pkg/gofr/datasource/kv-store/badger/mock_logger.go index 5bd0b8e79..0a32ba3b8 100644 --- a/pkg/gofr/datasource/kv-store/badger/mock_logger.go +++ b/pkg/gofr/datasource/kv-store/badger/mock_logger.go @@ -3,7 +3,7 @@ // // Generated by this command: // -// mockgen -source=logger.go -destination=mock_logger_old.go -package=badger +// mockgen -source=logger.go -destination=mock_logger.go -package=badger // // Package badger is a generated GoMock package. diff --git a/pkg/gofr/datasource/mongo/logger.go b/pkg/gofr/datasource/mongo/logger.go index 32c7e6c7a..05c39d1a8 100644 --- a/pkg/gofr/datasource/mongo/logger.go +++ b/pkg/gofr/datasource/mongo/logger.go @@ -9,6 +9,7 @@ import ( type Logger interface { Debug(args ...interface{}) + Debugf(pattern string, args ...interface{}) Logf(pattern string, args ...interface{}) Errorf(pattern string, args ...interface{}) } diff --git a/pkg/gofr/datasource/mongo/mock_logger.go b/pkg/gofr/datasource/mongo/mock_logger.go index 190431334..432d9253f 100644 --- a/pkg/gofr/datasource/mongo/mock_logger.go +++ b/pkg/gofr/datasource/mongo/mock_logger.go @@ -54,6 +54,23 @@ func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...) } +// Debugf mocks base method. +func (m *MockLogger) Debugf(pattern string, args ...any) { + m.ctrl.T.Helper() + varargs := []any{pattern} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debugf", varargs...) +} + +// Debugf indicates an expected call of Debugf. +func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{pattern}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...) +} + // Errorf mocks base method. func (m *MockLogger) Errorf(pattern string, args ...any) { m.ctrl.T.Helper() diff --git a/pkg/gofr/datasource/mongo/mongo.go b/pkg/gofr/datasource/mongo/mongo.go index 08c6d9d82..dd8c38ad2 100644 --- a/pkg/gofr/datasource/mongo/mongo.go +++ b/pkg/gofr/datasource/mongo/mongo.go @@ -33,9 +33,12 @@ type Config struct { Port int Database string // Deprecated Provide Host User Password Port Instead and driver will generate the URI - URI string + URI string + ConnectionTimeout time.Duration } +const defaultTimeout = 5 * time.Second + var errStatusDown = errors.New("status down") /* @@ -78,7 +81,7 @@ func (c *Client) UseTracer(tracer any) { // Connect establishes a connection to MongoDB and registers metrics using the provided configuration when the client was Created. func (c *Client) Connect() { - c.logger.Logf("connecting to mongoDB at %v to database %v", c.config.URI, c.config.Database) + c.logger.Debugf("connecting to MongoDB at %v to database %v", c.config.URI, c.config.Database) uri := c.config.URI @@ -87,17 +90,34 @@ func (c *Client) Connect() { c.config.User, c.config.Password, c.config.Host, c.config.Port, c.config.Database) } - m, err := mongo.Connect(context.Background(), options.Client().ApplyURI(uri)) + timeout := c.config.ConnectionTimeout + if timeout == 0 { + timeout = defaultTimeout + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + m, err := mongo.Connect(ctx, options.Client().ApplyURI(uri)) if err != nil { - c.logger.Errorf("error connecting to mongoDB, err:%v", err) + c.logger.Errorf("error while connecting to MongoDB, err:%v", err) return } + if err = m.Ping(ctx, nil); err != nil { + c.logger.Errorf("could not connect to mongoDB at %v due to err: %v", c.config.URI, err) + return + } + + c.logger.Logf("connected to mongoDB successfully at %v to database %v", c.config.URI, c.config.Database) + mongoBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10} c.metrics.NewHistogram("app_mongo_stats", "Response time of MONGO queries in milliseconds.", mongoBuckets...) c.Database = m.Database(c.config.Database) + + c.logger.Logf("connected to MongoDB at %v to database %v", uri, c.Database) } // InsertOne inserts a single document into the specified collection. @@ -265,7 +285,7 @@ func (c *Client) CreateCollection(ctx context.Context, name string) error { } func (c *Client) sendOperationStats(ql *QueryLog, startTime time.Time, method string, span trace.Span) { - duration := time.Since(startTime).Milliseconds() + duration := time.Since(startTime).Microseconds() ql.Duration = duration diff --git a/pkg/gofr/datasource/mongo/mongo_test.go b/pkg/gofr/datasource/mongo/mongo_test.go index 2e5e41b69..e485dee5f 100644 --- a/pkg/gofr/datasource/mongo/mongo_test.go +++ b/pkg/gofr/datasource/mongo/mongo_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -24,9 +25,13 @@ func Test_NewMongoClient(t *testing.T) { metrics.EXPECT().NewHistogram("app_mongo_stats", "Response time of MONGO queries in milliseconds.", gomock.Any()) - logger.EXPECT().Logf("connecting to mongoDB at %v to database %v", "", "test") + logger.EXPECT().Debugf(gomock.Any(), gomock.Any()) + logger.EXPECT().Logf(gomock.Any(), gomock.Any(), gomock.Any()) - client := New(Config{Database: "test", Host: "localhost", Port: 27017, User: "admin"}) + logger.EXPECT().Errorf(gomock.Any(), gomock.Any(), gomock.Any()) + + client := New(Config{Database: "test", Host: "localhost", Port: 27017, User: "admin", ConnectionTimeout: 1 * time.Second}) + client.Database = &mongo.Database{} client.UseLogger(logger) client.UseMetrics(metrics) client.Connect() @@ -41,8 +46,8 @@ func Test_NewMongoClientError(t *testing.T) { metrics := NewMockMetrics(ctrl) logger := NewMockLogger(ctrl) - logger.EXPECT().Logf("connecting to mongoDB at %v to database %v", "mongo", "test") - logger.EXPECT().Errorf("error connecting to mongoDB, err:%v", gomock.Any()) + logger.EXPECT().Debugf("connecting to MongoDB at %v to database %v", "mongo", "test") + logger.EXPECT().Errorf("error while connecting to MongoDB, err:%v", gomock.Any()) client := New(Config{URI: "mongo", Database: "test"}) client.UseLogger(logger) @@ -65,9 +70,9 @@ func Test_InsertCommands(t *testing.T) { cl := Client{metrics: metrics, tracer: otel.GetTracerProvider().Tracer("gofr-mongo")} metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", - gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(4) + gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(3) - logger.EXPECT().Debug(gomock.Any()).Times(4) + logger.EXPECT().Debug(gomock.Any()).Times(3) cl.logger = logger @@ -170,9 +175,9 @@ func Test_FindMultipleCommands(t *testing.T) { cl := Client{metrics: metrics, tracer: otel.GetTracerProvider().Tracer("gofr-mongo")} metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", - gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(3) + gomock.Any(), "database", gomock.Any(), "type", gomock.Any()) - logger.EXPECT().Debug(gomock.Any()).Times(3) + logger.EXPECT().Debug(gomock.Any()) cl.logger = logger @@ -244,9 +249,9 @@ func Test_FindOneCommands(t *testing.T) { cl := Client{metrics: metrics, tracer: otel.GetTracerProvider().Tracer("gofr-mongo")} metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", - gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(2) + gomock.Any(), "database", gomock.Any(), "type", gomock.Any()) - logger.EXPECT().Debug(gomock.Any()).Times(2) + logger.EXPECT().Debug(gomock.Any()) cl.logger = logger @@ -403,9 +408,9 @@ func Test_DeleteCommands(t *testing.T) { cl := Client{metrics: metrics, tracer: otel.GetTracerProvider().Tracer("gofr-mongo")} metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", - gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(4) + gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(2) - logger.EXPECT().Debug(gomock.Any()).Times(4) + logger.EXPECT().Debug(gomock.Any()).Times(2) cl.logger = logger diff --git a/pkg/gofr/datasource/opentsdb/go.mod b/pkg/gofr/datasource/opentsdb/go.mod new file mode 100644 index 000000000..e072e9c8c --- /dev/null +++ b/pkg/gofr/datasource/opentsdb/go.mod @@ -0,0 +1,19 @@ +module gofr.dev/gofr/pkg/gofr/datasource/opentsdb + +go 1.22.7 + +require ( + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/otel v1.30.0 + go.opentelemetry.io/otel/trace v1.30.0 + go.uber.org/mock v0.4.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/pkg/gofr/datasource/opentsdb/go.sum b/pkg/gofr/datasource/opentsdb/go.sum new file mode 100644 index 000000000..3f5f8dd7a --- /dev/null +++ b/pkg/gofr/datasource/opentsdb/go.sum @@ -0,0 +1,25 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/gofr/datasource/opentsdb/interface.go b/pkg/gofr/datasource/opentsdb/interface.go new file mode 100644 index 000000000..39ded7585 --- /dev/null +++ b/pkg/gofr/datasource/opentsdb/interface.go @@ -0,0 +1,52 @@ +package opentsdb + +import ( + "context" + "net" + "net/http" + "time" +) + +type connection interface { + Read(b []byte) (n int, err error) + Write(b []byte) (n int, err error) + Close() error + LocalAddr() net.Addr + RemoteAddr() net.Addr + SetDeadline(t time.Time) error + SetReadDeadline(t time.Time) error + SetWriteDeadline(t time.Time) error +} + +// HTTPClient is an interface that wraps the http.Client's Do method. +type httpClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// Response defines the common behaviors all the specific response for +// different rest-apis should obey. +// Currently, it is an abstraction used in Client.sendRequest() +// to stored the different kinds of response contents for all the rest-apis. +type response interface { + // getCustomParser can be used to retrieve a custom-defined parser. + // Returning nil means current specific Response instance doesn't + // need a custom-defined parse process, and just uses the default + // json unmarshal method to parse the contents of the http response. + getCustomParser(Logger) func(respCnt []byte) error +} + +// Logger interface is used by opentsdb package to log information about request execution. +type Logger interface { + Debug(args ...any) + Debugf(pattern string, args ...any) + Logf(pattern string, args ...any) + Log(args ...any) + Errorf(pattern string, args ...any) + Fatal(args ...any) +} + +type Metrics interface { + NewHistogram(name, desc string, buckets ...float64) + + RecordHistogram(ctx context.Context, name string, value float64, labels ...string) +} diff --git a/pkg/gofr/datasource/opentsdb/mock_interface.go b/pkg/gofr/datasource/opentsdb/mock_interface.go new file mode 100644 index 000000000..94014f509 --- /dev/null +++ b/pkg/gofr/datasource/opentsdb/mock_interface.go @@ -0,0 +1,411 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: interface.go +// +// Generated by this command: +// +// mockgen -source=interface.go -destination=mock_interface.go -package=opentsdb +// + +// Package opentsdb is a generated GoMock package. +package opentsdb + +import ( + context "context" + net "net" + http "net/http" + reflect "reflect" + time "time" + + gomock "go.uber.org/mock/gomock" +) + +// Mockconnection is a mock of connection interface. +type Mockconnection struct { + ctrl *gomock.Controller + recorder *MockconnectionMockRecorder +} + +// MockconnectionMockRecorder is the mock recorder for Mockconnection. +type MockconnectionMockRecorder struct { + mock *Mockconnection +} + +// NewMockconnection creates a new mock instance. +func NewMockconnection(ctrl *gomock.Controller) *Mockconnection { + mock := &Mockconnection{ctrl: ctrl} + mock.recorder = &MockconnectionMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *Mockconnection) EXPECT() *MockconnectionMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *Mockconnection) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockconnectionMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*Mockconnection)(nil).Close)) +} + +// LocalAddr mocks base method. +func (m *Mockconnection) LocalAddr() net.Addr { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LocalAddr") + ret0, _ := ret[0].(net.Addr) + return ret0 +} + +// LocalAddr indicates an expected call of LocalAddr. +func (mr *MockconnectionMockRecorder) LocalAddr() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LocalAddr", reflect.TypeOf((*Mockconnection)(nil).LocalAddr)) +} + +// Read mocks base method. +func (m *Mockconnection) Read(b []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", b) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockconnectionMockRecorder) Read(b any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*Mockconnection)(nil).Read), b) +} + +// RemoteAddr mocks base method. +func (m *Mockconnection) RemoteAddr() net.Addr { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoteAddr") + ret0, _ := ret[0].(net.Addr) + return ret0 +} + +// RemoteAddr indicates an expected call of RemoteAddr. +func (mr *MockconnectionMockRecorder) RemoteAddr() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoteAddr", reflect.TypeOf((*Mockconnection)(nil).RemoteAddr)) +} + +// SetDeadline mocks base method. +func (m *Mockconnection) SetDeadline(t time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetDeadline", t) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetDeadline indicates an expected call of SetDeadline. +func (mr *MockconnectionMockRecorder) SetDeadline(t any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeadline", reflect.TypeOf((*Mockconnection)(nil).SetDeadline), t) +} + +// SetReadDeadline mocks base method. +func (m *Mockconnection) SetReadDeadline(t time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetReadDeadline", t) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetReadDeadline indicates an expected call of SetReadDeadline. +func (mr *MockconnectionMockRecorder) SetReadDeadline(t any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReadDeadline", reflect.TypeOf((*Mockconnection)(nil).SetReadDeadline), t) +} + +// SetWriteDeadline mocks base method. +func (m *Mockconnection) SetWriteDeadline(t time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetWriteDeadline", t) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetWriteDeadline indicates an expected call of SetWriteDeadline. +func (mr *MockconnectionMockRecorder) SetWriteDeadline(t any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetWriteDeadline", reflect.TypeOf((*Mockconnection)(nil).SetWriteDeadline), t) +} + +// Write mocks base method. +func (m *Mockconnection) Write(b []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Write", b) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Write indicates an expected call of Write. +func (mr *MockconnectionMockRecorder) Write(b any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*Mockconnection)(nil).Write), b) +} + +// MockhttpClient is a mock of httpClient interface. +type MockhttpClient struct { + ctrl *gomock.Controller + recorder *MockhttpClientMockRecorder +} + +// MockhttpClientMockRecorder is the mock recorder for MockhttpClient. +type MockhttpClientMockRecorder struct { + mock *MockhttpClient +} + +// NewMockhttpClient creates a new mock instance. +func NewMockhttpClient(ctrl *gomock.Controller) *MockhttpClient { + mock := &MockhttpClient{ctrl: ctrl} + mock.recorder = &MockhttpClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockhttpClient) EXPECT() *MockhttpClientMockRecorder { + return m.recorder +} + +// Do mocks base method. +func (m *MockhttpClient) Do(req *http.Request) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Do", req) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Do indicates an expected call of Do. +func (mr *MockhttpClientMockRecorder) Do(req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockhttpClient)(nil).Do), req) +} + +// Mockresponse is a mock of response interface. +type Mockresponse struct { + ctrl *gomock.Controller + recorder *MockresponseMockRecorder +} + +// MockresponseMockRecorder is the mock recorder for Mockresponse. +type MockresponseMockRecorder struct { + mock *Mockresponse +} + +// NewMockresponse creates a new mock instance. +func NewMockresponse(ctrl *gomock.Controller) *Mockresponse { + mock := &Mockresponse{ctrl: ctrl} + mock.recorder = &MockresponseMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *Mockresponse) EXPECT() *MockresponseMockRecorder { + return m.recorder +} + +// getCustomParser mocks base method. +func (m *Mockresponse) getCustomParser(arg0 Logger) func([]byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "getCustomParser", arg0) + ret0, _ := ret[0].(func([]byte) error) + return ret0 +} + +// getCustomParser indicates an expected call of getCustomParser. +func (mr *MockresponseMockRecorder) getCustomParser(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getCustomParser", reflect.TypeOf((*Mockresponse)(nil).getCustomParser), arg0) +} + +// MockLogger is a mock of Logger interface. +type MockLogger struct { + ctrl *gomock.Controller + recorder *MockLoggerMockRecorder +} + +// MockLoggerMockRecorder is the mock recorder for MockLogger. +type MockLoggerMockRecorder struct { + mock *MockLogger +} + +// NewMockLogger creates a new mock instance. +func NewMockLogger(ctrl *gomock.Controller) *MockLogger { + mock := &MockLogger{ctrl: ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLogger) EXPECT() *MockLoggerMockRecorder { + return m.recorder +} + +// Debug mocks base method. +func (m *MockLogger) Debug(args ...any) { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debug", varargs...) +} + +// Debug indicates an expected call of Debug. +func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...) +} + +// Debugf mocks base method. +func (m *MockLogger) Debugf(pattern string, args ...any) { + m.ctrl.T.Helper() + varargs := []any{pattern} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debugf", varargs...) +} + +// Debugf indicates an expected call of Debugf. +func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{pattern}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...) +} + +// Errorf mocks base method. +func (m *MockLogger) Errorf(pattern string, args ...any) { + m.ctrl.T.Helper() + varargs := []any{pattern} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Errorf", varargs...) +} + +// Errorf indicates an expected call of Errorf. +func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{pattern}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...) +} + +// Fatal mocks base method. +func (m *MockLogger) Fatal(args ...any) { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Fatal", varargs...) +} + +// Fatal indicates an expected call of Fatal. +func (mr *MockLoggerMockRecorder) Fatal(args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fatal", reflect.TypeOf((*MockLogger)(nil).Fatal), args...) +} + +// Log mocks base method. +func (m *MockLogger) Log(args ...any) { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Log", varargs...) +} + +// Log indicates an expected call of Log. +func (mr *MockLoggerMockRecorder) Log(args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockLogger)(nil).Log), args...) +} + +// Logf mocks base method. +func (m *MockLogger) Logf(pattern string, args ...any) { + m.ctrl.T.Helper() + varargs := []any{pattern} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Logf", varargs...) +} + +// Logf indicates an expected call of Logf. +func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{pattern}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...) +} + +// MockMetrics is a mock of Metrics interface. +type MockMetrics struct { + ctrl *gomock.Controller + recorder *MockMetricsMockRecorder +} + +// MockMetricsMockRecorder is the mock recorder for MockMetrics. +type MockMetricsMockRecorder struct { + mock *MockMetrics +} + +// NewMockMetrics creates a new mock instance. +func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics { + mock := &MockMetrics{ctrl: ctrl} + mock.recorder = &MockMetricsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder { + return m.recorder +} + +// NewHistogram mocks base method. +func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) { + m.ctrl.T.Helper() + varargs := []any{name, desc} + for _, a := range buckets { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "NewHistogram", varargs...) +} + +// NewHistogram indicates an expected call of NewHistogram. +func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{name, desc}, buckets...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...) +} + +// RecordHistogram mocks base method. +func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) { + m.ctrl.T.Helper() + varargs := []any{ctx, name, value} + for _, a := range labels { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "RecordHistogram", varargs...) +} + +// RecordHistogram indicates an expected call of RecordHistogram. +func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, name, value}, labels...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...) +} diff --git a/pkg/gofr/datasource/opentsdb/observability.go b/pkg/gofr/datasource/opentsdb/observability.go new file mode 100644 index 000000000..b08ac6127 --- /dev/null +++ b/pkg/gofr/datasource/opentsdb/observability.go @@ -0,0 +1,106 @@ +package opentsdb + +import ( + "context" + "fmt" + "io" + "regexp" + "strings" + "time" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// QueryLog handles logging with different levels. +type QueryLog struct { + Operation string `json:"operation"` + Duration int64 `json:"duration"` + Status *string `json:"status"` + Message *string `json:"message,omitempty"` +} + +var regexpSpaces = regexp.MustCompile(`\s+`) + +func clean(query *string) string { + if query == nil { + return "" + } + + return strings.TrimSpace(regexpSpaces.ReplaceAllString(*query, " ")) +} + +func (ql *QueryLog) PrettyPrint(writer io.Writer) { + fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;148m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %-10s \u001B[0m %-48s \n", + clean(&ql.Operation), "OPENTSDB", ql.Duration, clean(ql.Status), clean(ql.Message)) +} + +func sendOperationStats(logger Logger, start time.Time, operation string, status, message *string, span trace.Span) { + duration := time.Since(start).Microseconds() + + logger.Debug(&QueryLog{ + Operation: operation, + Status: status, + Duration: duration, + Message: message, + }) + + if span != nil { + span.SetAttributes(attribute.Int64(fmt.Sprintf("opentsdb.%v.duration", operation), duration)) + span.End() + } +} + +func addTracer(ctx context.Context, tracer trace.Tracer, operation, typeName string) trace.Span { + if tracer == nil { + return nil + } + + _, span := tracer.Start(ctx, fmt.Sprintf("opentsdb-%s", operation)) + + span.SetAttributes( + attribute.String(fmt.Sprintf("opentsdb-%s.operation", typeName), operation), + ) + + return span +} + +func (c *Client) addTrace(ctx context.Context, operation string) trace.Span { + return addTracer(ctx, c.tracer, operation, "Client") +} + +func (*AggregatorsResponse) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span { + return addTracer(ctx, tracer, operation, "AggregatorRes") +} + +func (*AnnotationResponse) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span { + return addTracer(ctx, tracer, operation, "AnnotationRes") +} + +func (*QueryResponse) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span { + return addTracer(ctx, tracer, operation, "QueryResponse") +} + +func (*QueryRespItem) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span { + return addTracer(ctx, tracer, operation, "QueryRespItem") +} + +func (*QueryParam) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span { + return addTracer(ctx, tracer, operation, "QueryParam") +} + +func (*QueryLastParam) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span { + return addTracer(ctx, tracer, operation, "QueryLastParam") +} + +func (*QueryLastResponse) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span { + return addTracer(ctx, tracer, operation, "QueryLastResponse") +} + +func (*VersionResponse) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span { + return addTracer(ctx, tracer, operation, "VersionResponse") +} + +func (*PutResponse) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span { + return addTracer(ctx, tracer, operation, "PutResponse") +} diff --git a/pkg/gofr/datasource/opentsdb/opentsdb.go b/pkg/gofr/datasource/opentsdb/opentsdb.go new file mode 100644 index 000000000..6d9a067c8 --- /dev/null +++ b/pkg/gofr/datasource/opentsdb/opentsdb.go @@ -0,0 +1,420 @@ +// Package opentsdb provides a client implementation for interacting with OpenTSDB +// via its REST API. +package opentsdb + +import ( + "bytes" + "context" + "errors" + "fmt" + "net" + "net/http" + "net/url" + "time" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +const ( + statusFailed = "FAIL" + statusSuccess = "SUCCESS" + defaultDialTime = 5 * time.Second // Default time for establishing TCP connections. + connectionTimeout = 30 * time.Second // Timeout for keeping connections alive. + + // API paths for OpenTSDB endpoints. + putPath = "/api/put" + aggregatorPath = "/api/aggregators" + versionPath = "/api/version" + annotationPath = "/api/annotation" + queryPath = "/api/query" + queryLastPath = "/api/query/last" + + putRespWithSummary = "summary" // Summary response for PUT operations. + putRespWithDetails = "details" // Detailed response for PUT operations. + + // The three keys in the rateOption parameter of the QueryParam. + queryRateOptionCounter = "counter" // The corresponding value type is bool + queryRateOptionCounterMax = "counterMax" // The corresponding value type is int,int64 + queryRateOptionResetValue = "resetValue" // The corresponding value type is int,int64 + + anQueryStartTime = "start_time" + anQueryTSUid = "tsuid" + + // The below three constants are used in /put. + defaultMaxPutPointsNum = 75 + defaultDetectDeltaNum = 3 + // Unit is bytes, and assumes that config items of 'tsd.http.request.enable_chunked = true' + // and 'tsd.http.request.max_chunk = 40960' are all in the opentsdb.conf. + defaultMaxContentLength = 40960 +) + +var dialTimeout = net.DialTimeout + +// Client is the implementation of the OpenTSDBClient interface, +// which includes context-aware functionality. +type Client struct { + endpoint string + client httpClient + config Config + logger Logger + metrics Metrics + tracer trace.Tracer +} + +type Config struct { + + // The host of the target opentsdb, is a required non-empty string which is + // in the format of ip:port without http:// prefix or a domain. + Host string + + // A pointer of http.Transport is used by the opentsdb client. + // This value is optional, and if it is not set, client.defaultTransport, which + // enables tcp keepalive mode, will be used in the opentsdb client. + Transport *http.Transport + + // The maximal number of datapoints which will be inserted into the opentsdb + // via one calling of /api/put method. + // This value is optional, and if it is not set, client.defaultMaxPutPointsNum + // will be used in the opentsdb client. + MaxPutPointsNum int + + // The detect delta number of datapoints which will be used in client.Put() + // to split a large group of datapoints into small batches. + // This value is optional, and if it is not set, client.defaultDetectDeltaNum + // will be used in the opentsdb client. + DetectDeltaNum int + + // The maximal body content length per /api/put method to insert datapoints + // into opentsdb. + // This value is optional, and if it is not set, client.defaultMaxPutPointsNum + // will be used in the opentsdb client. + MaxContentLength int +} + +type Health struct { + Status string `json:"status,omitempty"` + Details map[string]any `json:"details,omitempty"` +} + +// New initializes a new instance of Opentsdb with provided configuration. +func New(config *Config) *Client { + return &Client{config: *config} +} + +func (c *Client) UseLogger(logger interface{}) { + if l, ok := logger.(Logger); ok { + c.logger = l + } +} + +func (c *Client) UseMetrics(metrics interface{}) { + if m, ok := metrics.(Metrics); ok { + c.metrics = m + } +} + +func (c *Client) UseTracer(tracer any) { + if tracer, ok := tracer.(trace.Tracer); ok { + c.tracer = tracer + } +} + +// Connect initializes an HTTP client for OpenTSDB using the provided configuration. +// If the configuration is invalid or the endpoint is unreachable, an error is logged. +func (c *Client) Connect() { + span := c.addTrace(context.Background(), "Connect") + + if span != nil { + span.SetAttributes(attribute.Int64(fmt.Sprintf("opentsdb.%v", "Connect"), 0)) + span.End() + } + + c.logger.Debugf("connecting to OpenTSDB at host %s", c.config.Host) + + // Set default values for optional configuration fields. + c.initializeClient() + + // Initialize the OpenTSDB client with the given configuration. + c.endpoint = fmt.Sprintf("http://%s", c.config.Host) + + res := VersionResponse{} + err := c.version(context.Background(), &res) + if err != nil { + c.logger.Errorf("error while connecting to OpenTSDB: %v", err) + return + } + + c.logger.Logf("connected to OpenTSDB at %s", c.endpoint) +} + +func (c *Client) PutDataPoints(ctx context.Context, datas any, queryParam string, resp any) error { + span := c.addTrace(ctx, "PutDataPoints") + + status := statusFailed + + message := "Put request failed" + + defer sendOperationStats(c.logger, time.Now(), "PutDataPoints", &status, &message, span) + + putResp, ok := resp.(*PutResponse) + if !ok { + return errors.New("invalid response type. Must be *PutResponse") + } + + datapoints, ok := datas.([]DataPoint) + if !ok { + return errors.New("invalid response type. Must be []DataPoint") + } + + err := validateDataPoint(datapoints) + if err != nil { + message = fmt.Sprintf("invalid data: %s", err) + return err + } + + if !isValidPutParam(queryParam) { + message = "the given query param is invalid." + return errors.New(message) + } + + putEndpoint := fmt.Sprintf("%s%s", c.endpoint, putPath) + if !isEmptyPutParam(queryParam) { + putEndpoint = fmt.Sprintf("%s?%s", putEndpoint, queryParam) + } + + tempResp, err := c.getResponse(ctx, putEndpoint, datapoints, &message) + if err != nil { + return err + } + + if len(tempResp.Errors) > 0 { + return parsePutErrorMsg(tempResp) + } + + status = statusSuccess + message = fmt.Sprintf("put request to url %q processed successfully", putEndpoint) + *putResp = *tempResp + + return nil +} + +func (c *Client) QueryDataPoints(ctx context.Context, parameters, resp any) error { + span := c.addTrace(ctx, "QueryDataPoints") + + status := statusFailed + + message := "QueryDatapoints request failed" + + defer sendOperationStats(c.logger, time.Now(), "Query", &status, &message, span) + + param, ok := parameters.(*QueryParam) + if !ok { + return errors.New("invalid parameter type. Must be *QueryParam") + } + + queryResp, ok := resp.(*QueryResponse) + if !ok { + return errors.New("invalid response type. Must be *QueryResponse") + } + + if !isValidQueryParam(param) { + message = "invalid query parameters" + return errors.New(message) + } + + queryEndpoint := fmt.Sprintf("%s%s", c.endpoint, queryPath) + + reqBodyCnt, err := getQueryBodyContents(param) + if err != nil { + message = fmt.Sprintf("getQueryBodyContents error: %s", err) + return err + } + + if err = c.sendRequest(ctx, http.MethodPost, queryEndpoint, reqBodyCnt, queryResp); err != nil { + message = fmt.Sprintf("error while processing request at url %q: %s ", queryEndpoint, err) + return err + } + + status = statusSuccess + message = fmt.Sprintf("query request at url %q processed successfully", queryEndpoint) + + return nil +} + +func (c *Client) QueryLatestDataPoints(ctx context.Context, parameters, resp any) error { + span := c.addTrace(ctx, "QueryLastDataPoints") + + status := statusFailed + + message := "QueryLatestDataPoints request failed" + + defer sendOperationStats(c.logger, time.Now(), "QueryLastDataPoints", &status, &message, span) + + param, ok := parameters.(*QueryLastParam) + if !ok { + return errors.New("invalid parameter type. Must be a *QueryLastParam type") + } + + queryResp, ok := resp.(*QueryLastResponse) + if !ok { + return errors.New("invalid response type. Must be a *QueryLastResponse type") + } + + if !isValidQueryLastParam(param) { + message = "invalid query last param" + return errors.New(message) + } + + queryEndpoint := fmt.Sprintf("%s%s", c.endpoint, queryLastPath) + + reqBodyCnt, err := getQueryBodyContents(param) + if err != nil { + message = fmt.Sprint("error retrieving body contents: ", err) + return err + } + + if err = c.sendRequest(ctx, http.MethodPost, queryEndpoint, reqBodyCnt, queryResp); err != nil { + message = fmt.Sprintf("error sending request at url %s : %s ", queryEndpoint, err) + return err + } + + status = statusSuccess + message = fmt.Sprintf("querylast request to url %q processed successfully", queryEndpoint) + + c.logger.Logf("querylast request processed successfully") + + return nil +} + +func (c *Client) QueryAnnotation(ctx context.Context, queryAnnoParam map[string]any, resp any) error { + span := c.addTrace(ctx, "QueryAnnotation") + + status := statusFailed + + message := "QueryAnnotation request failed" + + defer sendOperationStats(c.logger, time.Now(), "QueryAnnotation", &status, &message, span) + + annResp, ok := resp.(*AnnotationResponse) + if !ok { + return errors.New("invalid response type. Must be *AnnotationResponse") + } + + if len(queryAnnoParam) == 0 { + message = "annotation query parameter is empty" + return errors.New(message) + } + + buffer := bytes.NewBuffer(nil) + + queryURL := url.Values{} + + for k, v := range queryAnnoParam { + value, ok := v.(string) + if ok { + queryURL.Add(k, value) + } + } + + buffer.WriteString(queryURL.Encode()) + + annoEndpoint := fmt.Sprintf("%s%s?%s", c.endpoint, annotationPath, buffer.String()) + + if err := c.sendRequest(ctx, http.MethodGet, annoEndpoint, "", annResp); err != nil { + message = fmt.Sprintf("error while processing annotation query: %s", err.Error()) + return err + } + + status = statusSuccess + message = fmt.Sprintf("Annotation query sent to url: %s", annoEndpoint) + + c.logger.Log("Annotation query processed successfully") + + return nil +} + +func (c *Client) PostAnnotation(ctx context.Context, annotation, resp any) error { + return c.operateAnnotation(ctx, annotation, resp, http.MethodPost, "PostAnnotation") +} + +func (c *Client) PutAnnotation(ctx context.Context, annotation, resp any) error { + return c.operateAnnotation(ctx, annotation, resp, http.MethodPut, "PutAnnotation") +} + +func (c *Client) DeleteAnnotation(ctx context.Context, annotation, resp any) error { + return c.operateAnnotation(ctx, annotation, resp, http.MethodDelete, "DeleteAnnotation") +} + +func (c *Client) GetAggregators(ctx context.Context, resp any) error { + span := c.addTrace(ctx, "GetAggregators") + + status := statusFailed + + message := "GetAggregators request failed" + + defer sendOperationStats(c.logger, time.Now(), "GetAggregators", &status, &message, span) + + aggreResp, ok := resp.(*AggregatorsResponse) + if !ok { + return errors.New("invalid response type. Must be a *AggregatorsResponse") + } + + aggregatorsEndpoint := fmt.Sprintf("%s%s", c.endpoint, aggregatorPath) + + if err := c.sendRequest(ctx, http.MethodGet, aggregatorsEndpoint, "", aggreResp); err != nil { + message = fmt.Sprintf("error retrieving aggregators from url: %s", aggregatorsEndpoint) + return err + } + + status = statusSuccess + message = fmt.Sprintf("aggregators retrieved from url: %s", aggregatorsEndpoint) + + c.logger.Log("aggregators fetched successfully") + + return nil +} + +func (c *Client) HealthCheck(ctx context.Context) (any, error) { + span := c.addTrace(ctx, "HealthCheck") + + status := statusFailed + + message := "HealthCheck request failed" + + defer sendOperationStats(c.logger, time.Now(), "HealthCheck", &status, &message, span) + + h := Health{ + Details: make(map[string]any), + } + + conn, err := dialTimeout("tcp", c.config.Host, defaultDialTime) + if err != nil { + h.Status = "DOWN" + message = fmt.Sprintf("OpenTSDB is unreachable: %v", err) + + return nil, errors.New(message) + } + + if conn != nil { + defer conn.Close() + } + + h.Details["host"] = c.endpoint + + ver := &VersionResponse{} + + err = c.version(ctx, ver) + if err != nil { + message = err.Error() + return nil, err + } + + h.Details["version"] = ver.VersionInfo["version"] + + status = statusSuccess + h.Status = "UP" + message = "connection to OpenTSDB is alive" + + return &h, nil +} diff --git a/pkg/gofr/datasource/opentsdb/opentsdb_test.go b/pkg/gofr/datasource/opentsdb/opentsdb_test.go new file mode 100644 index 000000000..6015521db --- /dev/null +++ b/pkg/gofr/datasource/opentsdb/opentsdb_test.go @@ -0,0 +1,616 @@ +package opentsdb + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "math/rand/v2" + "net" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.uber.org/mock/gomock" +) + +var ( + errConnection = errors.New("connection error") + errRequestFailed = errors.New("request failed") +) + +func TestSendRequestSuccess(t *testing.T) { + client, mockHTTP := setOpenTSDBTest(t) + + mockResponse := http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`["sum","avg"]`)), + } + + mockHTTP.EXPECT(). + Do(gomock.Any()). + Return(&mockResponse, nil). + Times(1) + + parsedResp := AggregatorsResponse{} + + err := client.sendRequest(context.Background(), "GET", "http://localhost:4242/aggregators", "", &parsedResp) + + require.NoError(t, err) + assert.Equal(t, []string{"sum", "avg"}, parsedResp.Aggregators) +} + +func TestSendRequestFailure(t *testing.T) { + client, mockHTTP := setOpenTSDBTest(t) + + mockHTTP.EXPECT(). + Do(gomock.Any()). + Return(nil, errRequestFailed). + Times(1) + + parsedResp := AggregatorsResponse{} + + err := client.sendRequest(context.Background(), "GET", "http://localhost:4242/aggregators", "", &parsedResp) + + require.Error(t, err) + assert.Contains(t, err.Error(), "request failed") +} + +func TestGetCustomParser(t *testing.T) { + client, _ := setOpenTSDBTest(t) + + resp := &AggregatorsResponse{} + + parser := resp.getCustomParser(client.logger) + + err := parser([]byte(`["sum","avg"]`)) + + require.NoError(t, err) + assert.Equal(t, []string{"sum", "avg"}, resp.Aggregators) +} + +// setOpenTSDBTest initializes an Client for testing. +func setOpenTSDBTest(t *testing.T) (*Client, *MockhttpClient) { + t.Helper() + + opentsdbCfg := Config{ + Host: "localhost:4242", + MaxContentLength: 4096, + MaxPutPointsNum: 1000, + DetectDeltaNum: 10, + } + + tsdbClient := New(&opentsdbCfg) + + tracer := otel.GetTracerProvider().Tracer("gofr-opentsdb") + + tsdbClient.UseTracer(tracer) + + mocklogger := NewMockLogger(gomock.NewController(t)) + + tsdbClient.UseLogger(mocklogger) + + mocklogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mocklogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mocklogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + mocklogger.EXPECT().Log(gomock.Any()).AnyTimes() + + tsdbClient.config.Host = strings.TrimSpace(tsdbClient.config.Host) + if tsdbClient.config.Host == "" { + tsdbClient.logger.Errorf("the OpentsdbEndpoint in the given configuration cannot be empty.") + } + + mockhttp := NewMockhttpClient(gomock.NewController(t)) + + tsdbClient.client = mockhttp + + // Set default values for optional configuration fields. + if tsdbClient.config.MaxPutPointsNum <= 0 { + tsdbClient.config.MaxPutPointsNum = defaultMaxPutPointsNum + } + + if tsdbClient.config.DetectDeltaNum <= 0 { + tsdbClient.config.DetectDeltaNum = defaultDetectDeltaNum + } + + if tsdbClient.config.MaxContentLength <= 0 { + tsdbClient.config.MaxContentLength = defaultMaxContentLength + } + + // Initialize the OpenTSDB client with the given configuration. + tsdbClient.endpoint = fmt.Sprintf("http://%s", tsdbClient.config.Host) + + return tsdbClient, mockhttp +} + +func TestPutSuccess(t *testing.T) { + client, mockHTTP := setOpenTSDBTest(t) + + PutDataPointNum := 4 + name := []string{"cpu", "disk", "net", "mem"} + cpuDatas := make([]DataPoint, 0) + + tags := map[string]string{ + "host": "gofr-host", + "try-name": "gofr-sample", + "demo-name": "opentsdb-test", + } + + for i := 0; i < PutDataPointNum; i++ { + data := DataPoint{ + Metric: name[i%len(name)], + Timestamp: time.Now().Unix(), + Value: rand.Float64() * 100, + Tags: tags, + } + cpuDatas = append(cpuDatas, data) + } + + mockHTTP.EXPECT(). + Do(gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"StatusCode":200,"failed":0,"success":4}`)), + }, nil).Times(1) + + resp := &PutResponse{} + + err := client.PutDataPoints(context.Background(), cpuDatas, "details", resp) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, int64(len(cpuDatas)), resp.Success) +} + +func TestPutInvalidDataPoint(t *testing.T) { + client, _ := setOpenTSDBTest(t) + + dataPoints := []DataPoint{ + { + Metric: "", + Timestamp: 0, + Value: 0, + Tags: map[string]string{}, + }, + } + + resp := &PutResponse{} + + err := client.PutDataPoints(context.Background(), dataPoints, "", resp) + require.Error(t, err) + require.Equal(t, "the value of the given datapoint is invalid", err.Error()) +} + +func TestPutInvalidQueryParam(t *testing.T) { + client, _ := setOpenTSDBTest(t) + + dataPoints := []DataPoint{ + { + Metric: "metric1", + Timestamp: time.Now().Unix(), + Value: 100, + Tags: map[string]string{"tag1": "value1"}, + }, + } + + resp := &PutResponse{} + + err := client.PutDataPoints(context.Background(), dataPoints, "invalid_param", resp) + require.Error(t, err) + require.Equal(t, "the given query param is invalid.", err.Error()) +} + +func TestPutErrorResponse(t *testing.T) { + client, mockHTTP := setOpenTSDBTest(t) + + dataPoints := []DataPoint{ + { + Metric: "invalid_metric_name#$%", + Timestamp: time.Now().Unix(), + Value: 100, + Tags: map[string]string{"tag1": "value1"}, + }, + } + + mockHTTP.EXPECT(). + Do(gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusBadRequest, + Body: io.NopCloser(strings.NewReader(`{"StatusCode":400,"error":"Invalid metric name"}`)), + }, nil).Times(1) + + resp := &PutResponse{} + + err := client.PutDataPoints(context.Background(), dataPoints, "", resp) + require.Error(t, err) + require.Equal(t, "client error: 400", err.Error()) +} + +func TestPostQuerySuccess(t *testing.T) { + client, mockHTTP := setOpenTSDBTest(t) + + st1 := time.Now().Unix() - 3600 + st2 := time.Now().Unix() + queryParam := QueryParam{ + Start: st1, + End: st2, + } + + name := []string{"cpu", "disk", "net", "mem"} + subqueries := make([]SubQuery, 0) + tags := map[string]string{ + "host": "gofr-host", + "try-name": "gofr-sample", + "demo-name": "opentsdb-test", + } + + for _, metric := range name { + subQuery := SubQuery{ + Aggregator: "sum", + Metric: metric, + Tags: tags, + } + subqueries = append(subqueries, subQuery) + } + + queryParam.Queries = subqueries + + mockHTTP.EXPECT(). + Do(gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`[{"metric":"net","timestamp":1728836485000,"value":` + + `"19.499737232159088","tags":{"demo-name":"opentsdb-test","host":"gofr-host","try-name":` + + `"gofr-sample"},"tsuid":"000003000001000001000002000007000003000008"},{"metric":"disk",` + + `"timestamp":1728836485000,"value":"98.53097270356102","tags":{"demo-name":"opentsdb-test",` + + `"host":"gofr-host","try-name":"gofr-sample"},"tsuid":"000002000001000001000002000007000003000008"}` + + `,{"metric":"cpu","timestamp":1728836485000,"value":"49.47446557839882","tags":{"demo-name":"opentsdb` + + `-test","host":"gofr-host","try-name":"gofr-sample"},"tsuid":"000001000001000001000002000007000003000008"}` + + `,{"metric":"mem","timestamp":1728836485000,"value":"28.62340008609452","tags":{"demo-name":"opentsdb-test",` + + `"host":"gofr-host","try-name":"gofr-sample"},"tsuid":"000004000001000001000002000007000003000008"}]`)), + }, nil).Times(1) + + queryResp := &QueryResponse{} + err := client.QueryDataPoints(context.Background(), &queryParam, queryResp) + require.NoError(t, err) + require.NotNil(t, queryResp) + + require.Len(t, queryResp.QueryRespCnts, 4) +} + +func TestPostQueryLastSuccess(t *testing.T) { + client, mockHTTP := setOpenTSDBTest(t) + + name := []string{"cpu", "disk", "net", "mem"} + subqueriesLast := make([]SubQueryLast, 0) + tags := map[string]string{ + "host": "gofr-host", + "try-name": "gofr-sample", + "demo-name": "opentsdb-test", + } + + for _, metric := range name { + subQueryLast := SubQueryLast{ + Metric: metric, + Tags: tags, + } + subqueriesLast = append(subqueriesLast, subQueryLast) + } + + queryLastParam := QueryLastParam{ + Queries: subqueriesLast, + ResolveNames: true, + BackScan: 24, + } + + mockHTTP.EXPECT(). + Do(gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`[{"metric":"net","timestamp":1728836485000,"value":` + + `"19.499737232159088","tags":{"demo-name":"opentsdb-test","host":"gofr-host","try-name":` + + `"gofr-sample"},"tsuid":"000003000001000001000002000007000003000008"},{"metric":"disk",` + + `"timestamp":1728836485000,"value":"98.53097270356102","tags":{"demo-name":"opentsdb-test",` + + `"host":"gofr-host","try-name":"gofr-sample"},"tsuid":"000002000001000001000002000007000003000008"}` + + `,{"metric":"cpu","timestamp":1728836485000,"value":"49.47446557839882","tags":{"demo-name":"opentsdb` + + `-test","host":"gofr-host","try-name":"gofr-sample"},"tsuid":"000001000001000001000002000007000003000008"}` + + `,{"metric":"mem","timestamp":1728836485000,"value":"28.62340008609452","tags":{"demo-name":"opentsdb-test",` + + `"host":"gofr-host","try-name":"gofr-sample"},"tsuid":"000004000001000001000002000007000003000008"}]`)), + }, nil).Times(1) + + queryLastResp := &QueryLastResponse{} + + err := client.QueryLatestDataPoints(context.Background(), &queryLastParam, queryLastResp) + require.NoError(t, err) + require.NotNil(t, queryLastResp) + + require.Len(t, queryLastResp.QueryRespCnts, 4) +} + +func TestPostQueryDeleteSuccess(t *testing.T) { + client, mockHTTP := setOpenTSDBTest(t) + + st1 := time.Now().Unix() - 3600 + st2 := time.Now().Unix() + queryParam := QueryParam{ + Start: st1, + End: st2, + Delete: true, + } + + name := []string{"cpu", "disk", "net", "mem"} + subqueries := make([]SubQuery, 0) + tags := map[string]string{ + "host": "gofr-host", + "try-name": "gofr-sample", + "demo-name": "opentsdb-test", + } + + for _, metric := range name { + subQuery := SubQuery{ + Aggregator: "sum", + Metric: metric, + Tags: tags, + } + subqueries = append(subqueries, subQuery) + } + + queryParam.Queries = subqueries + + mockHTTP.EXPECT(). + Do(gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`[]`)), + }, nil).Times(1) + + deleteResp := &QueryResponse{} + + err := client.QueryDataPoints(context.Background(), &queryParam, deleteResp) + require.NoError(t, err) + require.NotNil(t, deleteResp) +} + +func TestGetAggregatorsSuccess(t *testing.T) { + client, mockHTTP := setOpenTSDBTest(t) + + expectedResponse := `["sum","avg","max","min","count"]` + + mockHTTP.EXPECT(). + Do(gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(expectedResponse)), // Response body with aggregators + }, nil).Times(1) + + aggreResp := &AggregatorsResponse{} + + err := client.GetAggregators(context.Background(), aggreResp) + require.NoError(t, err) + require.NotNil(t, aggreResp) + + var aggregators []string + err = json.Unmarshal([]byte(expectedResponse), &aggregators) + require.NoError(t, err) + require.ElementsMatch(t, aggregators, aggreResp.Aggregators) // Assuming your response has an Aggregators field +} + +func TestGetVersionSuccess(t *testing.T) { + client, mockHTTP := setOpenTSDBTest(t) + + expectedResponse := `{"short_revision":"","repo":"/opt/opentsdb/opentsdb-2.4.0/build",` + + `"host":"a0d1ce2d1fd7","version":"2.4.0","full_revision":"","repo_status":"MODIFIED"` + + `,"user":"root","branch":"","timestamp":"1607178614"}` + + mockHTTP.EXPECT(). + Do(gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(expectedResponse)), // Response body for version + }, nil).Times(1) + + versionResp := &VersionResponse{} + + err := client.version(context.Background(), versionResp) + require.NoError(t, err) + require.NotNil(t, versionResp) + + var versionData struct { + Version string `json:"version"` + } + + err = json.Unmarshal([]byte(expectedResponse), &versionData) + require.NoError(t, err) + + require.Equal(t, versionData.Version, versionResp.VersionInfo["version"]) +} + +func TestUpdateAnnotationSuccess(t *testing.T) { + client, mockHTTP := setOpenTSDBTest(t) + + custom := map[string]string{ + "owner": "gofr", + "host": "gofr-host", + } + addedST := time.Now().Unix() + addedTsuid := "000001000001000002" + anno := Annotation{ + StartTime: addedST, + TSUID: addedTsuid, + Description: "gofrf test annotation", + Notes: "These would be details about the event, the description is just a summary", + Custom: custom, + } + + expectedResponse := `{"tsuid":"000001000001000002","description":"gofrf test annotation","notes":` + + `"These would be details about the event, the description is just a summary","custom":{"host":` + + `"gofr-host","owner":"gofr"},"startTime":` + fmt.Sprintf("%d", addedST) + `,"endTime":0}` + + mockHTTP.EXPECT(). + Do(gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(expectedResponse)), // Response body for the annotation + }, nil).Times(1) + + queryAnnoResp := &AnnotationResponse{} + + err := client.PostAnnotation(context.Background(), &anno, queryAnnoResp) + + require.NoError(t, err) + require.NotNil(t, queryAnnoResp) + + require.Equal(t, anno.TSUID, queryAnnoResp.TSUID) + require.Equal(t, anno.StartTime, queryAnnoResp.StartTime) + require.Equal(t, anno.Description, queryAnnoResp.Description) + require.Equal(t, anno.Notes, queryAnnoResp.Notes) + require.Equal(t, anno.Custom, queryAnnoResp.Custom) +} + +func TestQueryAnnotationSuccess(t *testing.T) { + client, mockHTTP := setOpenTSDBTest(t) + + custom := map[string]string{ + "owner": "gofr", + "host": "gofr-host", + } + addedST := time.Now().Unix() + addedTsuid := "000001000001000002" + anno := Annotation{ + StartTime: addedST, + TSUID: addedTsuid, + Description: "gofr test annotation", + Notes: "These would be details about the event, the description is just a summary", + Custom: custom, + } + + mockResponse := `{"tsuid":"000001000001000002","description":"gofr test annotation","notes":"These` + + ` would be details about the event, the description is just a summary","custom":{"host"` + + `:"gofr-host","owner":"gofr"},"startTime":1728841614,"endTime":0}` + + mockHTTP.EXPECT(). + Do(gomock.Any()). + DoAndReturn(func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(mockResponse)), // Fresh body each time + }, nil + }).Times(2) + + postResp := &AnnotationResponse{} + + err := client.PostAnnotation(context.Background(), &anno, postResp) + require.NoError(t, err) + require.NotNil(t, postResp) + + queryAnnoMap := map[string]interface{}{ + anQueryStartTime: addedST, + anQueryTSUid: addedTsuid, + } + + queryResp := &AnnotationResponse{} + + err = client.QueryAnnotation(context.Background(), queryAnnoMap, queryResp) + require.NoError(t, err) + require.NotNil(t, queryResp) +} + +func TestDeleteAnnotationSuccess(t *testing.T) { + client, mockHTTP := setOpenTSDBTest(t) + + custom := map[string]string{ + "owner": "gofr", + "host": "gofr-host", + } + addedST := time.Now().Unix() + addedTsuid := "000001000001000002" + anno := Annotation{ + StartTime: addedST, + TSUID: addedTsuid, + Description: "gofr-host test annotation", + Notes: "These would be details about the event, the description is just a summary", + Custom: custom, + } + + mockHTTP.EXPECT(). + Do(gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{ + "tsuid": "000001000001000002", + "description": "gofr-host test annotation", + "notes": "These would be details about the event, the description is just a summary", + "custom": {"host": "gofr-host", "owner": "gofr"}, + "startTime": 1728843749, + "endTime": 0 + }`)), + }, nil).Times(1) + + postResp := &AnnotationResponse{} + + err := client.PostAnnotation(context.Background(), &anno, postResp) + require.NoError(t, err) + require.NotNil(t, postResp) + + mockHTTP.EXPECT(). + Do(gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusNoContent, + Body: http.NoBody, + }, nil).Times(1) + + deleteResp := &AnnotationResponse{} + + err = client.DeleteAnnotation(context.Background(), &anno, deleteResp) + require.NoError(t, err) + require.NotNil(t, deleteResp) + + require.Empty(t, deleteResp.TSUID) + require.Empty(t, deleteResp.StartTime) + require.Empty(t, deleteResp.Description) +} + +func TestHealthCheck_Success(t *testing.T) { + client, mockHTTP := setOpenTSDBTest(t) + + mockHTTP.EXPECT(). + Do(gomock.Any()). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"version": "2.9.0"}`)), + }, nil). + Times(1) + + mockConn := NewMockconnection(gomock.NewController(t)) + + mockConn.EXPECT().Close() + + dialTimeout = func(_, _ string, _ time.Duration) (net.Conn, error) { + return mockConn, nil + } + + resp, err := client.HealthCheck(context.Background()) + + require.NoError(t, err, "Expected no error during health check") + require.NotNil(t, resp, "Expected response to be not nil") + require.Equal(t, "UP", resp.(*Health).Status, "Expected status to be UP") + require.Equal(t, "2.9.0", resp.(*Health).Details["version"], "Expected version to be 2.9.0") +} + +func TestHealthCheck_Failure(t *testing.T) { + client, _ := setOpenTSDBTest(t) + + dialTimeout = func(_, _ string, _ time.Duration) (net.Conn, error) { + return nil, errConnection + } + + resp, err := client.HealthCheck(context.Background()) + + require.Error(t, err, "Expected error during health check") + require.Nil(t, resp, "Expected response to be nil") + require.Equal(t, "OpenTSDB is unreachable: connection error", err.Error(), "Expected specific error message") +} diff --git a/pkg/gofr/datasource/opentsdb/preprocess.go b/pkg/gofr/datasource/opentsdb/preprocess.go new file mode 100644 index 000000000..ca50544c7 --- /dev/null +++ b/pkg/gofr/datasource/opentsdb/preprocess.go @@ -0,0 +1,392 @@ +package opentsdb + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "strings" +) + +// defaultTransport defines the default HTTP transport settings, +// including connection timeouts and idle connections. +var defaultTransport = &http.Transport{ + MaxIdleConnsPerHost: 10, + DialContext: (&net.Dialer{ + Timeout: defaultDialTime, + KeepAlive: connectionTimeout, + }).DialContext, +} + +// QueryParam is the structure used to hold the querying parameters when calling /api/query. +// Each attributes in QueryParam matches the definition in +// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/index.html. +type QueryParam struct { + // The start time for the query. This can be a relative or absolute timestamp. + // The data type can only be string, int, or int64. + // The value is required with non-zero value of the target type. + Start any `json:"start"` + + // An end time for the query. If not supplied, the TSD will assume the local + // system time on the server. This may be a relative or absolute timestamp. + // The data type can only be string, or int64. + // The value is optional. + End any `json:"end,omitempty"` + + // One or more sub queries used to select the time series to return. + // These may be metric m or TSUID tsuids queries + // The value is required with at least one element + Queries []SubQuery `json:"queries"` + + // An optional value is used to show whether to return annotations with a query. + // The default is to return annotations for the requested timespan but this flag can disable the return. + // This affects both local and global notes and overrides globalAnnotations + NoAnnotations bool `json:"noAnnotations,omitempty"` + + // An optional value is used to show whether the query should retrieve global + // annotations for the requested timespan. + GlobalAnnotations bool `json:"globalAnnotations,omitempty"` + + // An optional value is used to show whether to output data point timestamps in milliseconds or seconds. + // If this flag is not provided and there are multiple data points within a second, + // those data points will be down sampled using the query's aggregation function. + MsResolution bool `json:"msResolution,omitempty"` + + // An optional value is used to show whether to output the TSUIDs associated with time series in the results. + // If multiple time series were aggregated into one set, multiple TSUIDs will be returned in a sorted manner. + ShowTSUIDs bool `json:"showTSUIDs,omitempty"` + + // An optional value is used to show whether can be passed to the JSON with a POST to delete any data point + // that match the given query. + Delete bool `json:"delete,omitempty"` +} + +// SubQuery is the structure used to hold the subquery parameters when calling /api/query. +// Each attributes in SubQuery matches the definition in +// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/index.html. +type SubQuery struct { + // The name of an aggregation function to use. + // The value is required with non-empty one in the range of + // the response of calling /api/aggregators. + // + // By default, the potential values and corresponding descriptions are as followings: + // "sum": Adds all of the data points for a timestamp. + // "min": Picks the smallest data point for each timestamp. + // "max": Picks the largest data point for each timestamp. + // "avg": Averages the values for the data points at each timestamp. + Aggregator string `json:"aggregator"` + + // The name of a metric stored in the system. + // The value is required with non-empty value. + Metric string `json:"metric"` + + // An optional value is used to show whether the data should be + // converted into deltas before returning. This is useful if the metric is a + // continuously incrementing counter, and you want to view the rate of change between data points. + Rate bool `json:"rate,omitempty"` + + // rateOptions represents monotonically increasing counter handling options. + // The value is optional. + // Currently, there is only three kind of value can be set to this map: + // Only three keys can be set into the rateOption parameter of the QueryParam is + // queryRateOptionCounter (value type is bool), queryRateOptionCounterMax (value type is int,int64) + // queryRateOptionResetValue (value type is int,int64) + RateParams map[string]any `json:"rateOptions,omitempty"` + + // An optional value downsampling function to reduce the amount of data returned. + DownSample string `json:"downsample,omitempty"` + + // An optional value to drill down to specific time series or group results by tag, + // supply one or more map values in the same format as the query string. Tags are converted to filters in 2.2. + // Note that if no tags are specified, all metrics in the system will be aggregated into the results. + // It will be deprecated in OpenTSDB 2.2. + Tags map[string]string `json:"tags,omitempty"` + + // An optional value used to filter the time series emitted in the results. + // Note that if no filters are specified, all time series for the given + // metric will be aggregated into the results. + Fiters []Filter `json:"filters,omitempty"` +} + +// Filter is the structure used to hold the filter parameters when calling /api/query. +// Each attributes in Filter matches the definition in +// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/index.html. +type Filter struct { + // The name of the filter to invoke. The value is required with a non-empty + // value in the range of calling /api/config/filters. + Type string `json:"type"` + + // The tag key to invoke the filter on, required with a non-empty value + Tagk string `json:"tagk"` + + // The filter expression to evaluate and depends on the filter being used, required with a non-empty value + FilterExp string `json:"filter"` + + // An optional value to show whether to group the results by each value matched by the filter. + // By default, all values matching the filter will be aggregated into a single series. + GroupBy bool `json:"groupBy"` +} + +// DataPoint is the structure used to hold the values of a metric item. Each attributes +// in DataPoint matches the definition in [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/put.html. +type DataPoint struct { + // The name of the metric which is about to be stored, and is required with non-empty value. + Metric string `json:"metric"` + + // A Unix epoch style timestamp in seconds or milliseconds. + // The timestamp must not contain non-numeric characters. + // One can use time.Now().Unix() to set this attribute. + // This attribute is also required with non-zero value. + Timestamp int64 `json:"timestamp"` + + // The real type of Value only could be int, int64, float64, or string, and is required. + Value any `json:"value"` + + // A map of tag name/tag value pairs. At least one pair must be supplied. + // Don't use too many tags, keep it to a fairly small number, usually up to 4 or 5 tags + // (By default, OpenTSDB supports a maximum of 8 tags, which can be modified by add + // configuration item 'tsd.storage.max_tags' in opentsdb.conf). + Tags map[string]string `json:"tags"` +} + +// PutError holds the error message for each putting DataPoint instance. Only calling PUT() with "details" +// query parameter, the response of the failed put data operation can contain an array PutError instance +// to show the details for each failure. +type PutError struct { + Data DataPoint `json:"datapoint"` + ErrorMsg string `json:"error"` +} + +// PutResponse acts as the implementation of Response in the /api/put scene. +// It holds the status code and the response values defined in +// the [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/put.html. +type PutResponse struct { + Failed int64 `json:"failed"` + Success int64 `json:"success"` + Errors []PutError `json:"errors,omitempty"` +} + +func (c *Client) getResponse(ctx context.Context, putEndpoint string, datapoints []DataPoint, + message *string) (*PutResponse, error) { + marshaled, err := json.Marshal(datapoints) + if err != nil { + *message = fmt.Sprintf("getPutBodyContents error: %s", err) + c.logger.Errorf(*message) + } + + reqBodyCnt := string(marshaled) + + putResp := PutResponse{} + + if err = c.sendRequest(ctx, http.MethodPost, putEndpoint, reqBodyCnt, &putResp); err != nil { + *message = fmt.Sprintf("error processing put request at url %q: %s", putEndpoint, err) + return nil, err + } + + return &putResp, nil +} + +func parsePutErrorMsg(resp *PutResponse) error { + buf := bytes.Buffer{} + buf.WriteString(fmt.Sprintf("Failed to put %d datapoint(s) into opentsdb \n", resp.Failed)) + + if len(resp.Errors) > 0 { + for _, putError := range resp.Errors { + str, err := json.Marshal(putError) + if err != nil { + return err + } + + buf.WriteString(fmt.Sprintf("\t%s\n", str)) + } + } + + return errors.New(buf.String()) +} + +func validateDataPoint(datas []DataPoint) error { + if len(datas) == 0 { + return errors.New("the given datapoint is empty") + } + + for _, data := range datas { + if !isValidDataPoint(&data) { + return errors.New("the value of the given datapoint is invalid") + } + } + + return nil +} + +func isValidDataPoint(data *DataPoint) bool { + if data.Metric == "" || data.Timestamp == 0 || len(data.Tags) < 1 || data.Value == nil { + return false + } + + switch data.Value.(type) { + case int64: + case int: + case float64: + case float32: + case string: + default: + return false + } + + return true +} + +func isValidPutParam(param string) bool { + if isEmptyPutParam(param) { + return true + } + + param = strings.TrimSpace(param) + if param != putRespWithSummary && param != putRespWithDetails { + return false + } + + return true +} + +func isEmptyPutParam(param string) bool { + return strings.TrimSpace(param) == "" +} + +// QueryLastParam is the structure used to hold +// the querying parameters when calling /api/query/last. +// Each attributes in QueryLastParam matches the definition in +// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/last.html. +type QueryLastParam struct { + // One or more sub queries used to select the time series to return. + // These may be metric m or TSUID tsuids queries + // The value is required with at least one element + Queries []SubQueryLast `json:"queries"` + + // An optional flag is used to determine whether or not to resolve the TSUIDs of results to + // their metric and tag names. The default value is false. + ResolveNames bool `json:"resolveNames"` + + // An optional number of hours is used to search in the past for data. If set to 0 then the + // timestamp of the meta data counter for the time series is used. + BackScan int `json:"backScan"` +} + +// SubQueryLast is the structure used to hold the subquery parameters when calling /api/query/last. +// Each attributes in SubQueryLast matches the definition in +// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/last.html. +type SubQueryLast struct { + // The name of a metric stored in the system. + // The value is required with non-empty value. + Metric string `json:"metric"` + + // An optional value to drill down to specific time series or group results by tag, + // supply one or more map values in the same format as the query string. Tags are converted to filters in 2.2. + // Note that if no tags are specified, all metrics in the system will be aggregated into the results. + // It will be deprecated in OpenTSDB 2.2. + Tags map[string]string `json:"tags,omitempty"` +} + +func getQueryBodyContents(param any) (string, error) { + result, err := json.Marshal(param) + if err != nil { + return "", fmt.Errorf("failed to marshal query param: %w", err) + } + + return string(result), nil +} + +func isValidQueryParam(param *QueryParam) bool { + if param.Queries == nil || len(param.Queries) == 0 { + return false + } + + if !isValidTimePoint(param.Start) { + return false + } + + for _, query := range param.Queries { + if !areValidParams(&query) { + return false + } + } + + return true +} + +func areValidParams(query *SubQuery) bool { + if query.Aggregator == "" || query.Metric == "" { + return false + } + + for k := range query.RateParams { + if k != queryRateOptionCounter && k != queryRateOptionCounterMax && k != queryRateOptionResetValue { + return false + } + } + + return true +} + +func isValidTimePoint(timePoint interface{}) bool { + if timePoint == nil { + return false + } + + switch v := timePoint.(type) { + case int: + return v > 0 + case int64: + return v > 0 + case string: + return v != "" + } + + return false +} + +func isValidQueryLastParam(param *QueryLastParam) bool { + if param.Queries == nil || len(param.Queries) == 0 { + return false + } + + for _, query := range param.Queries { + if query.Metric == "" { + return false + } + } + + return true +} + +func (c *Client) initializeClient() { + c.config.Host = strings.TrimSpace(c.config.Host) + if c.config.Host == "" { + c.logger.Fatal("the OpentsdbEndpoint in the given configuration cannot be empty.") + } + + // Use custom transport settings if provided, otherwise, use the default transport. + transport := c.config.Transport + if transport == nil { + transport = defaultTransport + } + + c.client = &http.Client{ + Transport: transport, + } + + if c.config.MaxPutPointsNum <= 0 { + c.config.MaxPutPointsNum = defaultMaxPutPointsNum + } + + if c.config.DetectDeltaNum <= 0 { + c.config.DetectDeltaNum = defaultDetectDeltaNum + } + + if c.config.MaxContentLength <= 0 { + c.config.MaxContentLength = defaultMaxContentLength + } +} diff --git a/pkg/gofr/datasource/opentsdb/response.go b/pkg/gofr/datasource/opentsdb/response.go new file mode 100644 index 000000000..89f10efdf --- /dev/null +++ b/pkg/gofr/datasource/opentsdb/response.go @@ -0,0 +1,374 @@ +package opentsdb + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + "go.opentelemetry.io/otel/trace" +) + +// AggregatorsResponse acts as the implementation of Response in the /api/aggregators. +// It holds the status code and the response values defined in the +// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/aggregators.html. +type AggregatorsResponse struct { + StatusCode int + Aggregators []string +} + +// VersionResponse is the struct implementation for /api/version. +type VersionResponse struct { + StatusCode int + VersionInfo map[string]any +} + +// Annotation holds parameters for querying or managing annotations via the /api/annotation endpoint in OpenTSDB. +// Used for logging notes on events at specific times, often tied to time series data, mainly for graphing or API queries. +type Annotation struct { + // StartTime is the Unix epoch timestamp (in seconds) for when the event occurred. This is required. + StartTime int64 `json:"startTime,omitempty"` + + // EndTime is the optional Unix epoch timestamp (in seconds) for when the event ended, if applicable. + EndTime int64 `json:"endTime,omitempty"` + + // TSUID is the optional time series identifier if the annotation is linked to a specific time series. + TSUID string `json:"tsuid,omitempty"` + + // Description is a brief, optional summary of the event (recommended to keep under 25 characters for display purposes). + Description string `json:"description,omitempty"` + + // Notes is an optional, detailed description of the event. + Notes string `json:"notes,omitempty"` + + // Custom is an optional key/value map to store any additional fields and their values. + Custom map[string]string `json:"custom,omitempty"` +} + +// AnnotationResponse encapsulates the response data and status when interacting with the /api/annotation endpoint. +type AnnotationResponse struct { + + // Annotation holds the associated annotation object. + Annotation + + // ErrorInfo contains details about any errors that occurred during the request. + ErrorInfo map[string]any `json:"error,omitempty"` +} + +// QueryResponse acts as the implementation of Response in the /api/query scene. +// It holds the status code and the response values defined in the +// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/index.html. +type QueryResponse struct { + QueryRespCnts []QueryRespItem `json:"queryRespCnts"` + ErrorMsg map[string]any `json:"error"` +} + +// QueryRespItem acts as the implementation of Response in the /api/query scene. +// It holds the response item defined in the +// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/index.html. +type QueryRespItem struct { + // Name of the metric retrieved for the time series + Metric string `json:"metric"` + + // A list of tags only returned when the results are for a single time series. + // If results are aggregated, this value may be null or an empty map + Tags map[string]string `json:"tags"` + + // If more than one time series were included in the result set, i.e. they were aggregated, + // this will display a list of tag names that were found in common across all time series. + // Note that: Api Doc uses 'aggregatedTags', but actual response uses 'aggregateTags' + AggregatedTags []string `json:"aggregateTags"` + + // Retrieved data points after being processed by the aggregators. Each data point consists + // of a timestamp and a value, the format determined by the serializer. + // For the JSON serializer, the timestamp will always be a Unix epoch style integer followed + // by the value as an integer or a floating point. + // For example, the default output is "dps"{"":}. + // By default, the timestamps will be in seconds. If the msResolution flag is set, then the + // timestamps will be in milliseconds. + // + // Because the elements of map is out of order, using common way to iterate Dps will not get + // data points with timestamps out of order. + // So be aware that one should use '(qri *QueryRespItem) GetDataPoints() []*DataPoint' to + // acquire the real ascending data points. + Dps map[string]any `json:"dps"` + + // If the query retrieved annotations for time series over the requested timespan, they will + // be returned in this group. Annotations for every time series will be merged into one set + // and sorted by start_time. Aggregator functions do not affect annotations, all annotations + // will be returned for the span. + // The value is optional. + Annotations []Annotation `json:"annotations,omitempty"` + + // If requested by the user, the query will scan for global annotations during + // the timespan and the results returned in this group. + // The value is optional. + GlobalAnnotations []Annotation `json:"globalAnnotations,omitempty"` +} + +// QueryLastResponse acts as the implementation of Response in the /api/query/last scene. +// It holds the status code and the response values defined in the +// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/last.html. +type QueryLastResponse struct { + QueryRespCnts []QueryRespLastItem `json:"queryRespCnts,omitempty"` + ErrorMsg map[string]any `json:"error"` +} + +// QueryRespLastItem acts as the implementation of Response in the /api/query/last scene. +// It holds the response item defined in the +// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/last.html. +type QueryRespLastItem struct { + // Name of the metric retreived for the time series. + // Only returned if resolve was set to true. + Metric string `json:"metric"` + + // A list of tags only returned when the results are for a single time series. + // If results are aggregated, this value may be null or an empty map. + // Only returned if resolve was set to true. + Tags map[string]string `json:"tags"` + + // A Unix epoch timestamp, in milliseconds, when the data point was written. + Timestamp int64 `json:"timestamp"` + + // The value of the data point enclosed in quotation marks as a string + Value string `json:"value"` + + // The hexadecimal TSUID for the time series + TSUID string `json:"tsuid"` +} + +func (*PutResponse) getCustomParser(Logger) func(respCnt []byte) error { + return nil +} + +func (queryResp *QueryResponse) getCustomParser(logger Logger) func(respCnt []byte) error { + return queryParserHelper(logger, queryResp, "GetCustomParser-Query") +} + +func (queryLastResp *QueryLastResponse) getCustomParser(logger Logger) func(respCnt []byte) error { + return queryParserHelper(logger, queryLastResp, "GetCustomParser-QueryLast") +} + +func (verResp *VersionResponse) getCustomParser(logger Logger) func(respCnt []byte) error { + return customParserHelper("GetCustomParser-VersionResp", logger, + func(resp []byte) error { + v := make(map[string]any, 0) + + err := json.Unmarshal(resp, &v) + if err != nil { + return err + } + + verResp.VersionInfo = v + + return nil + }) +} + +func (annotResp *AnnotationResponse) getCustomParser(logger Logger) func(respCnt []byte) error { + return customParserHelper("getCustomParser-Annotation", logger, + func(resp []byte) error { + if len(resp) == 0 { + return nil + } + + return json.Unmarshal(resp, &annotResp) + }) +} + +func (aggreResp *AggregatorsResponse) getCustomParser(logger Logger) func(respCnt []byte) error { + return customParserHelper("GetCustomParser-Aggregator", logger, + func(resp []byte) error { + j := make([]string, 0) + + err := json.Unmarshal(resp, &j) + if err != nil { + return err + } + + aggreResp.Aggregators = j + + return nil + }) +} + +type genericResponse interface { + addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span +} + +// sendRequest dispatches an HTTP request to the OpenTSDB server, using the provided +// method, URL, and body content. It returns the parsed response or an error, if any. +func (c *Client) sendRequest(ctx context.Context, method, url, reqBodyCnt string, parsedResp response) error { + // Create the HTTP request, attaching the context if available. + req, err := http.NewRequest(method, url, strings.NewReader(reqBodyCnt)) + if ctx != nil { + req = req.WithContext(ctx) + } + + if err != nil { + errRequestCreation := fmt.Errorf("failed to create request for %s %s: %w", method, url, err) + + return errRequestCreation + } + + // Set the request headers. + req.Header.Set("Content-Type", "application/json; charset=UTF-8") + + // Send the request and handle the response. + resp, err := c.client.Do(req) + if err != nil { + errSendingRequest := fmt.Errorf("failed to send request for %s %s: %w", method, url, err) + + return errSendingRequest + } + + defer resp.Body.Close() + + // Read and parse the response. + jsonBytes, err := io.ReadAll(resp.Body) + if err != nil { + errReading := fmt.Errorf("failed to read response body for %s %s: %w", method, url, err) + + return errReading + } + + parser := parsedResp.getCustomParser(c.logger) + if parser == nil { + // Use the default JSON unmarshaller if no custom parser is provided. + if err = json.Unmarshal(jsonBytes, parsedResp); err != nil { + errUnmarshaling := fmt.Errorf("failed to unmarshal response body for %s %s: %w", method, url, err) + + return errUnmarshaling + } + } else { + // Use the custom parser if available. + if err := parser(jsonBytes); err != nil { + return fmt.Errorf("failed to parse response body through custom parser %s %s: %w", method, url, err) + } + } + + if resp.StatusCode >= 400 && resp.StatusCode < 500 { + return fmt.Errorf("client error: %d", resp.StatusCode) + } + + return nil +} + +func (c *Client) version(ctx context.Context, verResp *VersionResponse) error { + span := c.addTrace(ctx, "Version") + + status := statusFailed + + message := "version request failed" + + defer sendOperationStats(c.logger, time.Now(), "Version", &status, &message, span) + + verEndpoint := fmt.Sprintf("%s%s", c.endpoint, versionPath) + + if err := c.sendRequest(ctx, http.MethodGet, verEndpoint, "", verResp); err != nil { + message = fmt.Sprintf("error while processing request at URL %s: %s", verEndpoint, err) + return err + } + + status = statusSuccess + message = fmt.Sprintf("OpenTSDB version %v", verResp.VersionInfo["version"]) + + return nil +} + +// isValidOperateMethod checks if the provided HTTP method is valid for +// operations such as POST, PUT, or DELETE. +func (c *Client) isValidOperateMethod(method string) bool { + method = strings.TrimSpace(strings.ToUpper(method)) + if method == "" { + return false + } + + validMethods := []string{http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPut} + for _, validMethod := range validMethods { + if method == validMethod { + return true + } + } + + return false +} + +func customParserHelper(operation string, logger Logger, unmarshalFunc func([]byte) error) func([]byte) error { + return func(result []byte) error { + err := unmarshalFunc(result) + if err != nil { + logger.Errorf("unmarshal %s error: %s", operation, err) + + return err + } + + return nil + } +} + +func queryParserHelper(logger Logger, obj genericResponse, + methodName string) func(respCnt []byte) error { + return customParserHelper(methodName, logger, func(resp []byte) error { + originRespStr := string(resp) + + var respStr string + + if strings.HasPrefix(string(resp), "[") && strings.HasSuffix(string(resp), "]") { + respStr = fmt.Sprintf(`{"queryRespCnts":%s}`, originRespStr) + } else { + respStr = originRespStr + } + + return json.Unmarshal([]byte(respStr), obj) + }) +} + +func (c *Client) operateAnnotation(ctx context.Context, queryAnnotation, resp any, method, operation string) error { + span := c.addTrace(ctx, operation) + + status := statusFailed + + message := fmt.Sprintf("%v request failed", operation) + + defer sendOperationStats(c.logger, time.Now(), operation, &status, &message, span) + + annotation, ok := queryAnnotation.(*Annotation) + if !ok { + return errors.New("invalid annotation type. Must be *Annotation") + } + + annResp, ok := resp.(*AnnotationResponse) + if !ok { + return errors.New("invalid response type. Must be *AnnotationResponse") + } + + if !c.isValidOperateMethod(method) { + message = fmt.Sprintf("invalid annotation operation method: %s", method) + return errors.New(message) + } + + annoEndpoint := fmt.Sprintf("%s%s", c.endpoint, annotationPath) + + resultBytes, err := json.Marshal(annotation) + if err != nil { + message = fmt.Sprintf("marshal annotation response error: %s", err) + return errors.New(message) + } + + if err = c.sendRequest(ctx, method, annoEndpoint, string(resultBytes), annResp); err != nil { + message = fmt.Sprintf("%s: error while processing %s annotation request to url %q: %s", operation, method, annoEndpoint, err.Error()) + return err + } + + status = statusSuccess + message = fmt.Sprintf("%s: %s annotation request to url %q processed successfully", operation, method, annoEndpoint) + + c.logger.Log("%s request successful", operation) + + return nil +} diff --git a/pkg/gofr/datasource/pubsub/eventhub/mock_logger.go b/pkg/gofr/datasource/pubsub/eventhub/mock_logger.go index 13f050b39..4ccf84665 100644 --- a/pkg/gofr/datasource/pubsub/eventhub/mock_logger.go +++ b/pkg/gofr/datasource/pubsub/eventhub/mock_logger.go @@ -3,10 +3,10 @@ // // Generated by this command: // -// mockgen -source=logger.go -destination=mock_logger.go -package=azeventhub +// mockgen -source=logger.go -destination=mock_logger.go -package=eventhub // -// Package azeventhub is a generated GoMock package. +// Package eventhub is a generated GoMock package. package eventhub import ( diff --git a/pkg/gofr/datasource/pubsub/kafka/kafka.go b/pkg/gofr/datasource/pubsub/kafka/kafka.go index e17067698..cbb317266 100644 --- a/pkg/gofr/datasource/pubsub/kafka/kafka.go +++ b/pkg/gofr/datasource/pubsub/kafka/kafka.go @@ -62,7 +62,7 @@ func New(conf Config, logger pubsub.Logger, metrics Metrics) *kafkaClient { return nil } - logger.Debugf("connecting to kafka broker '%s'", conf.Broker) + logger.Debugf("connecting to Kafka broker '%s'", conf.Broker) conn, err := kafka.Dial("tcp", conf.Broker) if err != nil { @@ -90,7 +90,7 @@ func New(conf Config, logger pubsub.Logger, metrics Metrics) *kafkaClient { reader := make(map[string]Reader) - logger.Logf("connected to kafka broker '%s'", conf.Broker) + logger.Logf("connected to Kafka broker '%s'", conf.Broker) return &kafkaClient{ config: conf, diff --git a/pkg/gofr/datasource/pubsub/nats/client.go b/pkg/gofr/datasource/pubsub/nats/client.go index fa0f49a91..2b58e651f 100644 --- a/pkg/gofr/datasource/pubsub/nats/client.go +++ b/pkg/gofr/datasource/pubsub/nats/client.go @@ -33,6 +33,8 @@ type messageHandler func(context.Context, jetstream.Msg) error // Connect establishes a connection to NATS and sets up jStream. func (c *Client) Connect() error { + c.logger.Debugf("connecting to NATS server at %v", c.Config.Server) + if err := c.validateAndPrepare(); err != nil { return err } diff --git a/pkg/gofr/datasource/pubsub/nats/connection_manager.go b/pkg/gofr/datasource/pubsub/nats/connection_manager.go index 0325b5c13..65561a431 100644 --- a/pkg/gofr/datasource/pubsub/nats/connection_manager.go +++ b/pkg/gofr/datasource/pubsub/nats/connection_manager.go @@ -84,8 +84,6 @@ func NewConnectionManager( // Connect establishes a connection to NATS and sets up JetStream. func (cm *ConnectionManager) Connect() error { - cm.logger.Debugf("Connecting to NATS server at %v", cm.config.Server) - opts := []nats.Option{nats.Name("GoFr NATS JetStreamClient")} if cm.config.CredsFile != "" { @@ -94,8 +92,6 @@ func (cm *ConnectionManager) Connect() error { connInterface, err := cm.natsConnector.Connect(cm.config.Server, opts...) if err != nil { - cm.logger.Errorf("failed to connect to NATS server at %v: %v", cm.config.Server, err) - return err } diff --git a/pkg/gofr/datasource/redis/hook.go b/pkg/gofr/datasource/redis/hook.go index cab12df2f..372f7f1f5 100644 --- a/pkg/gofr/datasource/redis/hook.go +++ b/pkg/gofr/datasource/redis/hook.go @@ -65,7 +65,7 @@ func (ql *QueryLog) String() string { // logQuery logs the Redis query information. func (r *redisHook) sendOperationStats(start time.Time, query string, args ...interface{}) { - duration := time.Since(start).Milliseconds() + duration := time.Since(start).Microseconds() r.logger.Debug(&QueryLog{ Query: query, diff --git a/pkg/gofr/datasource/solr/solr.go b/pkg/gofr/datasource/solr/solr.go index 68f53c4e5..f4976bdce 100644 --- a/pkg/gofr/datasource/solr/solr.go +++ b/pkg/gofr/datasource/solr/solr.go @@ -67,16 +67,32 @@ func (c *Client) UseTracer(tracer any) { // Connect establishes a connection to Solr and registers metrics using the provided configuration when the client was Created. func (c *Client) Connect() { - c.logger.Infof("connecting to Solr at %v", c.url) + c.logger.Debugf("connecting to Solr at %v", c.url) solrBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10} c.metrics.NewHistogram("app_solr_stats", "Response time of Solr operations in milliseconds.", solrBuckets...) + _, err := c.HealthCheck(context.Background()) + if err != nil { + c.logger.Errorf("error while connecting to Solr: %v", err) + return + } + + c.logger.Infof("connected to Solr at %v", c.url) + return } func (c *Client) HealthCheck(ctx context.Context) (any, error) { - return nil, nil + url := c.url + "/admin/info/system?wt=json" + + startTime := time.Now() + + resp, err, span := c.call(ctx, http.MethodGet, url, nil, nil) + + defer c.sendOperationStats(ctx, &QueryLog{Type: "HealthCheck", Url: url}, startTime, "healthcheck", span) + + return resp, err } // Search searches documents in the given collections based on the parameters specified. @@ -87,7 +103,7 @@ func (c *Client) Search(ctx context.Context, collection string, params map[strin resp, err, span := c.call(ctx, http.MethodGet, url, params, nil) - defer c.sendOperationStats(ctx, &QueryLog{Type: "Search", Url: url}, startTime, "search", span) + c.sendOperationStats(ctx, &QueryLog{Type: "Search", Url: url}, startTime, "search", span) return resp, err } @@ -100,7 +116,7 @@ func (c *Client) Create(ctx context.Context, collection string, document *bytes. resp, err, span := c.call(ctx, http.MethodPost, url, params, document) - defer c.sendOperationStats(ctx, &QueryLog{Type: "Create", Url: url}, startTime, "create", span) + c.sendOperationStats(ctx, &QueryLog{Type: "Create", Url: url}, startTime, "create", span) return resp, err } @@ -108,12 +124,12 @@ func (c *Client) Create(ctx context.Context, collection string, document *bytes. // Update updates documents in the specified collection. params can be used to send parameters like commit=true func (c *Client) Update(ctx context.Context, collection string, document *bytes.Buffer, params map[string]any) (any, error) { - url := c.url + collection + "/update" + url := c.url + "/" + collection + "/update" startTime := time.Now() resp, err, span := c.call(ctx, http.MethodPost, url, params, document) - defer c.sendOperationStats(ctx, &QueryLog{Type: "Update", Url: url}, startTime, "update", span) + c.sendOperationStats(ctx, &QueryLog{Type: "Update", Url: url}, startTime, "update", span) return resp, err } @@ -121,12 +137,12 @@ func (c *Client) Update(ctx context.Context, collection string, document *bytes. // Delete deletes documents in the specified collection. params can be used to send parameters like commit=true func (c *Client) Delete(ctx context.Context, collection string, document *bytes.Buffer, params map[string]any) (any, error) { - url := c.url + collection + "/update" + url := c.url + "/" + collection + "/update" startTime := time.Now() resp, err, span := c.call(ctx, http.MethodPost, url, params, document) - defer c.sendOperationStats(ctx, &QueryLog{Type: "Delete", Url: url}, startTime, "delete", span) + c.sendOperationStats(ctx, &QueryLog{Type: "Delete", Url: url}, startTime, "delete", span) return resp, err } @@ -134,12 +150,12 @@ func (c *Client) Delete(ctx context.Context, collection string, document *bytes. // ListFields retrieves all the fields in the schema for the specified collection. // params can be used to send query parameters like wt, fl, includeDynamic etc. func (c *Client) ListFields(ctx context.Context, collection string, params map[string]any) (any, error) { - url := c.url + collection + "/schema/fields" + url := c.url + "/" + collection + "/schema/fields" startTime := time.Now() resp, err, span := c.call(ctx, http.MethodGet, url, params, nil) - defer c.sendOperationStats(ctx, &QueryLog{Type: "ListFields", Url: url}, startTime, "list-fields", span) + c.sendOperationStats(ctx, &QueryLog{Type: "ListFields", Url: url}, startTime, "list-fields", span) return resp, err } @@ -147,48 +163,48 @@ func (c *Client) ListFields(ctx context.Context, collection string, params map[s // Retrieve retrieves the entire schema that includes all the fields,field types,dynamic rules and copy field rules. // params can be used to specify the format of response func (c *Client) Retrieve(ctx context.Context, collection string, params map[string]any) (any, error) { - url := c.url + collection + "/schema" + url := c.url + "/" + collection + "/schema" startTime := time.Now() resp, err, span := c.call(ctx, http.MethodGet, url, params, nil) - defer c.sendOperationStats(ctx, &QueryLog{Type: "Retrieve", Url: url}, startTime, "retrieve", span) + c.sendOperationStats(ctx, &QueryLog{Type: "Retrieve", Url: url}, startTime, "retrieve", span) return resp, err } // AddField adds Field in the schema for the specified collection func (c *Client) AddField(ctx context.Context, collection string, document *bytes.Buffer) (any, error) { - url := c.url + collection + "/schema" + url := c.url + "/" + collection + "/schema" startTime := time.Now() resp, err, span := c.call(ctx, http.MethodPost, url, nil, document) - defer c.sendOperationStats(ctx, &QueryLog{Type: "AddField", Url: url}, startTime, "add-field", span) + c.sendOperationStats(ctx, &QueryLog{Type: "AddField", Url: url}, startTime, "add-field", span) return resp, err } // UpdateField updates the field definitions in the schema for the specified collection func (c *Client) UpdateField(ctx context.Context, collection string, document *bytes.Buffer) (any, error) { - url := c.url + collection + "/schema" + url := c.url + "/" + collection + "/schema" startTime := time.Now() resp, err, span := c.call(ctx, http.MethodPost, url, nil, document) - defer c.sendOperationStats(ctx, &QueryLog{Type: "UpdateField", Url: url}, startTime, "update-field", span) + c.sendOperationStats(ctx, &QueryLog{Type: "UpdateField", Url: url}, startTime, "update-field", span) return resp, err } // DeleteField deletes the field definitions in the schema for the specified collection func (c *Client) DeleteField(ctx context.Context, collection string, document *bytes.Buffer) (any, error) { - url := c.url + collection + "/schema" + url := c.url + "/" + collection + "/schema" startTime := time.Now() resp, err, span := c.call(ctx, http.MethodPost, url, nil, document) - defer c.sendOperationStats(ctx, &QueryLog{Type: "DeleteField", Url: url}, startTime, "delete-field", span) + c.sendOperationStats(ctx, &QueryLog{Type: "DeleteField", Url: url}, startTime, "delete-field", span) return resp, err } @@ -274,7 +290,7 @@ func (c *Client) createRequest(ctx context.Context, method, url string, params m } func (c *Client) sendOperationStats(ctx context.Context, ql *QueryLog, startTime time.Time, method string, span trace.Span) { - duration := time.Since(startTime).Milliseconds() + duration := time.Since(startTime).Microseconds() ql.Duration = duration diff --git a/pkg/gofr/datasource/sql/db.go b/pkg/gofr/datasource/sql/db.go index e025f0b24..1399e5946 100644 --- a/pkg/gofr/datasource/sql/db.go +++ b/pkg/gofr/datasource/sql/db.go @@ -45,7 +45,7 @@ func clean(query string) string { } func (d *DB) sendOperationStats(start time.Time, queryType, query string, args ...interface{}) { - duration := time.Since(start).Milliseconds() + duration := time.Since(start).Microseconds() d.logger.Debug(&Log{ Type: queryType, @@ -129,7 +129,7 @@ type Tx struct { } func (t *Tx) sendOperationStats(start time.Time, queryType, query string, args ...interface{}) { - duration := time.Since(start).Milliseconds() + duration := time.Since(start).Microseconds() t.logger.Debug(&Log{ Type: queryType, diff --git a/pkg/gofr/datasource/sql/sql.go b/pkg/gofr/datasource/sql/sql.go index 90d0aacb2..6df8d98cf 100644 --- a/pkg/gofr/datasource/sql/sql.go +++ b/pkg/gofr/datasource/sql/sql.go @@ -207,11 +207,15 @@ func pushDBMetrics(db *sql.DB, metrics Metrics) { } func printConnectionSuccessLog(status string, dbconfig *DBConfig, logger datasource.Logger) { + logFunc := logger.Infof + if status != "connected" { + logFunc = logger.Debugf + } + if dbconfig.Dialect == sqlite { - logger.Infof("%s to '%s' database", status, dbconfig.Database) + logFunc("%s to '%s' database", status, dbconfig.Database) } else { - logger.Infof("%s to '%s' user to '%s' database at '%s:%s'", status, dbconfig.User, - dbconfig.Database, dbconfig.HostName, dbconfig.Port) + logFunc("%s to '%s' user to '%s' database at '%s:%s'", status, dbconfig.User, dbconfig.Database, dbconfig.HostName, dbconfig.Port) } } diff --git a/pkg/gofr/external_db.go b/pkg/gofr/external_db.go index 11706715e..4d56fb086 100644 --- a/pkg/gofr/external_db.go +++ b/pkg/gofr/external_db.go @@ -121,7 +121,26 @@ func (a *App) AddDgraph(db container.DgraphProvider) { db.UseLogger(a.Logger()) db.UseMetrics(a.Metrics()) + tracer := otel.GetTracerProvider().Tracer("gofr-dgraph") + + db.UseTracer(tracer) + db.Connect() a.container.DGraph = db } + +// AddOpentsdb sets the opentsdb datasource in the app's container. +func (a *App) AddOpenTSDB(db container.OpenTSDBProvider) { + // Create the Opentsdb client with the provided configuration + db.UseLogger(a.Logger()) + db.UseMetrics(a.Metrics()) + + tracer := otel.GetTracerProvider().Tracer("gofr-opentsdb") + + db.UseTracer(tracer) + + db.Connect() + + a.container.OpenTSDB = db +} diff --git a/pkg/gofr/external_db_test.go b/pkg/gofr/external_db_test.go index 7e2d38bc4..b16ca7da4 100644 --- a/pkg/gofr/external_db_test.go +++ b/pkg/gofr/external_db_test.go @@ -145,3 +145,23 @@ func TestApp_AddS3(t *testing.T) { assert.Equal(t, mock, app.container.File) }) } + +func TestApp_AddOpenTSDB(t *testing.T) { + t.Run("Adding OpenTSDB", func(t *testing.T) { + app := New() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mock := container.NewMockOpenTSDBProvider(ctrl) + + mock.EXPECT().UseLogger(app.Logger()) + mock.EXPECT().UseMetrics(app.Metrics()) + mock.EXPECT().UseTracer(gomock.Any()) + mock.EXPECT().Connect() + + app.AddOpenTSDB(mock) + + assert.Equal(t, mock, app.container.OpenTSDB) + }) +} diff --git a/pkg/gofr/gofr.go b/pkg/gofr/gofr.go index 979983016..2da25c9dc 100644 --- a/pkg/gofr/gofr.go +++ b/pkg/gofr/gofr.go @@ -648,6 +648,8 @@ func (a *App) UseMiddleware(middlewares ...gofrHTTP.Middleware) { // // The `middleware` function receives the container and the handler, allowing // the middleware to modify the request processing flow. +// Deprecated: UseMiddlewareWithContainer will be removed in a future release. +// Please use the [*App.UseMiddleware] method that does not depend on the container. func (a *App) UseMiddlewareWithContainer(middlewareHandler func(c *container.Container, handler http.Handler) http.Handler) { a.httpServer.router.Use(func(h http.Handler) http.Handler { // Wrap the provided handler `h` with the middleware function `middlewareHandler` @@ -688,6 +690,10 @@ func contains(elems []string, v string) bool { func (a *App) AddStaticFiles(endpoint, filePath string) { a.httpRegistered = true + if !strings.HasPrefix(filePath, "./") && !filepath.IsAbs(filePath) { + filePath = "./" + filePath + } + // update file path based on current directory if it starts with ./ if strings.HasPrefix(filePath, "./") { currentWorkingDir, _ := os.Getwd() @@ -701,5 +707,7 @@ func (a *App) AddStaticFiles(endpoint, filePath string) { return } + a.container.Logger.Infof("registered static files at endpoint '%s' from directory '%s'", endpoint, filePath) + a.httpServer.router.AddStaticFiles(endpoint, filePath) } diff --git a/pkg/gofr/gofr_test.go b/pkg/gofr/gofr_test.go index dd68aa7e9..c05c71d54 100644 --- a/pkg/gofr/gofr_test.go +++ b/pkg/gofr/gofr_test.go @@ -815,7 +815,7 @@ func TestStaticHandler(t *testing.T) { app := New() - app.AddStaticFiles("gofrTest", "./testdir") + app.AddStaticFiles("gofrTest", "testdir") app.httpRegistered = true app.httpServer.port = 8022 diff --git a/pkg/gofr/grpc/log.go b/pkg/gofr/grpc/log.go index dbcdbf808..d4b1a58b9 100644 --- a/pkg/gofr/grpc/log.go +++ b/pkg/gofr/grpc/log.go @@ -67,7 +67,7 @@ func LoggingInterceptor(logger Logger) grpc.UnaryServerInterceptor { l := RPCLog{ ID: trace.SpanFromContext(ctx).SpanContext().TraceID().String(), StartTime: start.Format("2006-01-02T15:04:05.999999999-07:00"), - ResponseTime: time.Since(start).Milliseconds(), + ResponseTime: time.Since(start).Microseconds(), Method: info.FullMethod, } diff --git a/pkg/gofr/handler.go b/pkg/gofr/handler.go index a2a609f5b..32a363cf3 100644 --- a/pkg/gofr/handler.go +++ b/pkg/gofr/handler.go @@ -80,17 +80,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { }() // Execute the handler function result, err = h.function(c) - - // Log the error(if any) with traceID and errorMessage - if err != nil { - errorLog := &ErrorLogEntry{ - TraceID: traceID, - Error: err.Error(), - } - - h.container.Logger.Error(errorLog) - } - + h.logError(traceID, err) close(done) }() @@ -101,14 +91,18 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { err = gofrHTTP.ErrorRequestTimeout{} } case <-done: - if websocket.IsWebSocketUpgrade(r) { - // Do not respond with HTTP headers since this is a WebSocket request - return - } + handleWebSocketUpgrade(r) case <-panicked: err = gofrHTTP.ErrorPanicRecovery{} } + // Handle custom headers if 'result' is a 'Response'. + if resp, ok := result.(response.Response); ok { + resp.SetCustomHeaders(w) + + result = resp.Data + } + // Handler function completed c.responder.Respond(result, err) } @@ -150,3 +144,18 @@ func panicRecoveryHandler(re any, log logging.Logger, panicked chan struct{}) { StackTrace: string(debug.Stack()), }) } + +// Log the error(if any) with traceID and errorMessage. +func (h handler) logError(traceID string, err error) { + if err != nil { + errorLog := &ErrorLogEntry{TraceID: traceID, Error: err.Error()} + h.container.Logger.Error(errorLog) + } +} + +func handleWebSocketUpgrade(r *http.Request) { + if websocket.IsWebSocketUpgrade(r) { + // Do not respond with HTTP headers since this is a WebSocket request + return + } +} diff --git a/pkg/gofr/handler_test.go b/pkg/gofr/handler_test.go index cc3d8ebf8..c786177b6 100644 --- a/pkg/gofr/handler_test.go +++ b/pkg/gofr/handler_test.go @@ -99,6 +99,68 @@ func TestHandler_ServeHTTP_Panic(t *testing.T) { assert.Contains(t, w.Body.String(), http.StatusText(http.StatusInternalServerError), "TestHandler_ServeHTTP_Panic Failed") } +func TestHandler_ServeHTTP_WithHeaders(t *testing.T) { + testCases := []struct { + desc string + method string + data any + headers map[string]string + err error + statusCode int + body string + }{ + { + desc: "Response with headers, method is GET, no error", + method: http.MethodGet, + data: response.Response{ + Headers: map[string]string{ + "X-Custom-Header": "custom-value", + "Content-Type": "application/json", + }, + Data: map[string]string{ + "message": "Hello, World!", + }, + }, + headers: map[string]string{ + "X-Custom-Header": "custom-value", + "Content-Type": "application/json", + }, + statusCode: http.StatusOK, + body: `{"message":"Hello, World!"}`, + }, + { + desc: "No headers, method is GET, data is simple string, no error", + method: http.MethodGet, + data: "simple string", + statusCode: http.StatusOK, + body: `"simple string"`, + }, + } + + for i, tc := range testCases { + w := httptest.NewRecorder() + r := httptest.NewRequest(tc.method, "/", http.NoBody) + c := &container.Container{ + Logger: logging.NewLogger(logging.FATAL), + } + + handler{ + function: func(*Context) (any, error) { + return tc.data, tc.err + }, + container: c, + }.ServeHTTP(w, r) + + assert.Containsf(t, w.Body.String(), tc.body, "TEST[%d], Failed.\n%s", i, tc.desc) + + assert.Equal(t, tc.statusCode, w.Code, "TEST[%d], Failed.\n%s", i, tc.desc) + + for key, expectedValue := range tc.headers { + assert.Equal(t, expectedValue, w.Header().Get(key), "TEST[%d], Failed. Header mismatch: %s", i, key) + } + } +} + func TestHandler_faviconHandlerError(t *testing.T) { c := Context{ Context: context.Background(), diff --git a/pkg/gofr/http/middleware/logger.go b/pkg/gofr/http/middleware/logger.go index 61e671169..42b3dc31d 100644 --- a/pkg/gofr/http/middleware/logger.go +++ b/pkg/gofr/http/middleware/logger.go @@ -140,6 +140,7 @@ func panicRecovery(re any, w http.ResponseWriter, logger logger) { default: e = "Unknown panic type" } + logger.Error(panicLog{ Error: e, StackTrace: string(debug.Stack()), diff --git a/pkg/gofr/http/request.go b/pkg/gofr/http/request.go index 2c493a7f6..cf68acf0f 100644 --- a/pkg/gofr/http/request.go +++ b/pkg/gofr/http/request.go @@ -21,6 +21,7 @@ const ( var ( errNoFileFound = errors.New("no files were bounded") errNonPointerBind = errors.New("bind error, cannot bind to a non pointer type") + errNonSliceBind = errors.New("bind error: input is not a pointer to a byte slice") ) // Request is an abstraction over the underlying http.Request. This abstraction is useful because it allows us @@ -70,6 +71,8 @@ func (r *Request) Bind(i interface{}) error { return r.bindMultipart(i) case "application/x-www-form-urlencoded": return r.bindFormURLEncoded(i) + case "binary/octet-stream": + return r.bindBinary(i) } return nil @@ -157,3 +160,22 @@ func (r *Request) bindForm(ptr any, isMultipart bool) error { return nil } + +// bindBinary handles binding for binary/octet-stream content type. +func (r *Request) bindBinary(raw interface{}) error { + // Ensure raw is a pointer to a byte slice + byteSlicePtr, ok := raw.(*[]byte) + if !ok { + return fmt.Errorf("%w: %v", errNonSliceBind, raw) + } + + body, err := r.body() + if err != nil { + return fmt.Errorf("failed to read request body: %w", err) + } + + // Assign the body to the provided slice + *byteSlicePtr = body + + return nil +} diff --git a/pkg/gofr/http/request_test.go b/pkg/gofr/http/request_test.go index d69832c87..31e98b2bf 100644 --- a/pkg/gofr/http/request_test.go +++ b/pkg/gofr/http/request_test.go @@ -3,6 +3,7 @@ package http import ( "bytes" "context" + "errors" "io" "mime/multipart" "net/http" @@ -272,3 +273,47 @@ func TestBind_FormURLEncoded(t *testing.T) { t.Errorf("Bind error. Got: %v", x) } } + +func TestBind_BinaryOctetStream(t *testing.T) { + testCases := []struct { + name string + data []byte + }{ + {"Raw Binary Data", []byte{0x42, 0x65, 0x6c, 0x6c, 0x61}}, + {"Text-Based Binary Data", []byte("This is some binary data")}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req := NewRequest(httptest.NewRequest(http.MethodPost, "/binary", bytes.NewReader(tc.data))) + req.req.Header.Set("Content-Type", "binary/octet-stream") + + var result []byte + + err := req.Bind(&result) + if err != nil { + t.Errorf("Bind error: %v", err) + } + + if !bytes.Equal(result, tc.data) { + t.Errorf("Bind error. Expected: %v, Got: %v", tc.data, result) + } + }) + } +} +func TestBind_BinaryOctetStream_NotPointerToByteSlice(t *testing.T) { + req := &Request{ + req: httptest.NewRequest(http.MethodPost, "/binary", http.NoBody), + } + req.req.Header.Set("Content-Type", "binary/octet-stream") + + err := req.Bind("invalid input") + + if !errors.Is(err, errNonSliceBind) { + t.Fatalf("Expected error: %v, got: %v", errNonSliceBind, err) + } + + if !strings.Contains(err.Error(), "input is not a pointer to a byte slice: invalid input") { + t.Errorf("Expected error to contain: input is not a pointer to a byte slice: invalid input, got: %v", err) + } +} diff --git a/pkg/gofr/http/response/response.go b/pkg/gofr/http/response/response.go new file mode 100644 index 000000000..702e513e4 --- /dev/null +++ b/pkg/gofr/http/response/response.go @@ -0,0 +1,16 @@ +package response + +import ( + "net/http" +) + +type Response struct { + Data any `json:"data"` + Headers map[string]string `json:"-"` +} + +func (resp Response) SetCustomHeaders(w http.ResponseWriter) { + for key, value := range resp.Headers { + w.Header().Set(key, value) + } +} diff --git a/pkg/gofr/service/new.go b/pkg/gofr/service/new.go index 2fcca104c..bf519ac1d 100644 --- a/pkg/gofr/service/new.go +++ b/pkg/gofr/service/new.go @@ -180,7 +180,7 @@ func (h *httpService) createAndSendRequest(ctx context.Context, method string, p respTime := time.Since(requestStart) - log.ResponseTime = respTime.Milliseconds() + log.ResponseTime = respTime.Microseconds() if err != nil { log.ResponseCode = http.StatusInternalServerError diff --git a/pkg/gofr/version/version.go b/pkg/gofr/version/version.go index a12560f2c..cd95fd59d 100644 --- a/pkg/gofr/version/version.go +++ b/pkg/gofr/version/version.go @@ -1,3 +1,3 @@ package version -const Framework = "v1.27.1" +const Framework = "v1.28.0"