diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b011d6d7..5d7c60b99 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,15 +1,15 @@ ## Contribution Guidelines -* Minor changes can be done directly by editing code on github. Github automatically creates a temporary branch and +* Minor changes can be done directly by editing code on GitHub. GitHub automatically creates a temporary branch and files a PR. This is only suitable for really small changes like: spelling fixes, variable name changes or error string change etc. For larger commits, following steps are recommended. -* (Optional) If you want to discuss your implementation with the users of Gofr, use the github discussions of this repo. +* (Optional) If you want to discuss your implementation with the users of GoFr, use the GitHub discussions of this repo. * Configure your editor to use goimport and golangci-lint on file changes. Any code which is not formatted using these tools, will fail on the pipeline. * All code contributions should have associated tests and all new line additions should be covered in those testcases. No PR should ever decrease the overall code coverage. * Once your code changes are done along with the testcases, submit a PR to development branch. Please note that all PRs are merged from feature branches to development first. -* All PRs need to be reviewed by at least 2 Gofr developers. They might reach out to you for any clarfication. +* All PRs need to be reviewed by at least 2 GoFr developers. They might reach out to you for any clarification. * Thank you for your contribution. :) ### GoFr Testing Policy: @@ -87,7 +87,7 @@ Please note that the recommended local port for the services are different than * Take interfaces and return concrete types. - Lean interfaces - take 'exactly' what you need, not more. Onus of interface definition is on the package who is using it. so, it should be as lean as possible. This makes it easier to test. - - Be careful of type assertions in this context. If you take an interface and type assert to a type - then its + - Be careful of type assertions in this context. If you take an interface and type assert to a type - then it's similar to taking concrete type. * Uses of context: - We should use context as a first parameter. @@ -113,4 +113,4 @@ Please note that the recommended local port for the services are different than - Use trailing white space or the
HTML tag at the end of the line. - Use "`" sign to add single line code and "```" to add multi-line code block. - Use relative references to images (in `public` folder as mentioned above.) -* The [gofr.dev documentation]([url](https://gofr.dev/docs)) site is updated upon push to `/docs` path in the repo. Verify your changes are live after next gofr version. +* The [gofr.dev documentation]([url](https://gofr.dev/docs)) site is updated upon push to `/docs` path in the repo. Verify your changes are live after next GoFr version. diff --git a/README.md b/README.md index 15286e488..b2cf3f07c 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,13 @@
-Gofr is an opinionated microservice development framework. Listed in [CNCF Landscape](https://landscape.cncf.io/?selected=go-fr). +GoFr is an opinionated microservice development framework. Listed in [CNCF Landscape](https://landscape.cncf.io/?selected=go-fr). Visit https://gofr.dev for more details and documentation. ## 🎯 Goal -Even though generic applications can be written using Gofr, our main focus is to simplify the development of microservices. -We will focus ourselves towards deployment in kubernetes and aspire to provide out-of-the-box observability. +Even though generic applications can be written using GoFr, our main focus is to simplify the development of microservices. +We will focus ourselves towards deployment in Kubernetes and aspire to provide out-of-the-box observability. ## 💡 Advantages/Features diff --git a/docs/advanced-guide/dealing-with-datasources/page.md b/docs/advanced-guide/dealing-with-datasources/page.md index 48169f34e..54e511b9d 100644 --- a/docs/advanced-guide/dealing-with-datasources/page.md +++ b/docs/advanced-guide/dealing-with-datasources/page.md @@ -1,13 +1,12 @@ # Dealing with SQL GoFr simplifies the process of connecting to SQL databases where one needs to add respective configs in .env, -which allows to connect to different SQL dialects(MYSQL, PostgreSQL) without going into complexity of configuring connections. +which allows connecting to different SQL dialects(MySQL, PostgreSQL, SQLite) without going into complexity of configuring connections. With GoFr, connecting to different SQL databases is as straightforward as setting the DB_DIALECT environment variable to the respective dialect. -For instance, to connect with PostgreSQL, set `DB_DIALECT` to `postgres`. Similarly, To connect with MySQL, simply set `DB_DIALECT` to `mysql`. -## Usage -Add the following configs in .env file. +## Usage for PostgreSQL and MySQL +To connect with PostgreSQL, set `DB_DIALECT` to `postgres`. Similarly, To connect with MySQL, simply set `DB_DIALECT` to `mysql`. ```dotenv DB_HOST=localhost @@ -17,4 +16,13 @@ DB_NAME=test_db DB_PORT=3306 DB_DIALECT=postgres +``` + +## Usage for SQLite +To connect with PostgreSQL, set `DB_DIALECT` to `sqlite` and `DB_NAME` to the name of your DB File. If the DB file already exists then it will be used otherwise a new one will be created. + +```dotenv +DB_NAME=test.db + +DB_DIALECT=sqlite ``` \ No newline at end of file diff --git a/docs/advanced-guide/gofr-errors/page.md b/docs/advanced-guide/gofr-errors/page.md new file mode 100644 index 000000000..4927e655f --- /dev/null +++ b/docs/advanced-guide/gofr-errors/page.md @@ -0,0 +1,56 @@ +# Error Handling + +GoFr provides a structured error handling approach to simplify error management in your applications. +The errors package in GoFr provides functionality for handling errors in GoFr applications. It includes predefined HTTP +and database errors, as well as the ability to create custom errors with additional context. + +## Pre-defined HTTP Errors + +GoFr’s `http` package offers several predefined error types to represent common HTTP error scenarios. These errors +automatically handle HTTP status code selection. These include: + +- `ErrorInvalidParam`: Represents an error due to an invalid parameter. +- `ErrorMissingParam`: Represents an error due to a missing parameter. +- `ErrorEntityNotFound`: Represents an error due to a not found entity. + +#### Usage: +To use the predefined http errors,users can simply call them using GoFr's http package: +```go + err := http.ErrorMissingParam{Param: []string{"id"}} +``` + +## Database Errors +Database errors in GoFr, represented in the `datasource` package, encapsulate errors related to database operations such +as database connection, query failure, availability etc. The `ErrorDB` struct can be used to populate `error` as well as +any custom message to it. + +#### Usage: +```go +// Creating a custom error wrapped in underlying error for database operations +dbErr := datasource.ErrorDB{Err: err, Message: "error from sql db"} + +// Adding stack trace to the error +dbErr = dbErr.WithStack() + +// Creating a custom error only with error message and no underlying error. +dbErr2 := datasource.ErrorDB{Message : "database connection timed out!"} +``` + +## Custom Errors +GoFr's error structs implements an interface with `Error() string` and `StatusCode() int` methods, users can override the +status code by implementing it for their custom error. + +#### Usage: +```go +type customError struct { + error string +} + +func (c customError) Error() string { + return fmt.Sprintf("custom error: %s", c.error) +} + +func (c customError) StatusCode() int { + return http.StatusMethodNotAllowed +} +``` diff --git a/docs/advanced-guide/grpc/page.md b/docs/advanced-guide/grpc/page.md index c29742d3d..61e4c8be4 100644 --- a/docs/advanced-guide/grpc/page.md +++ b/docs/advanced-guide/grpc/page.md @@ -11,13 +11,13 @@ framework initially developed by Google. $ apt install -y protobuf-compiler $ protoc --version # Ensure compiler version is 3+ ``` - - MacOS, using homebrew + - macOS, using Homebrew ```shell $ brew install protobuf $ protoc --version # Ensure compiler version is 3+ ``` - Install **Go Plugins** for protocol compiler: - 1. Install prtocol compiler plugins for Go + 1. Install protocol compiler plugins for Go ```shell $ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 $ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 @@ -30,7 +30,7 @@ framework initially developed by Google. ## Creating protocol buffers For a detailed guide, please take a look at the {% new-tab-link title="Tutorial" href="https://grpc.io/docs/languages/go/basics/" /%} at official gRPC docs. -We need to create a `customer.proto` file to define our service and the rpc methods that the service provides. +We need to create a `customer.proto` file to define our service and the RPC methods that the service provides. ```protobuf // Indicates the protocol buffer version that is being used syntax = "proto3"; @@ -76,7 +76,7 @@ protoc \ --go-grpc_opt=paths=source_relative \ customer.proto ``` -Above command will generate two files `customer.pb.go` and `customer_grpc.pb.go` and these contain necessary code to perform rpc calls. +Above command will generate two files `customer.pb.go` and `customer_grpc.pb.go` and these contain necessary code to perform RPC calls. In `customer.pb.go` you can find `CustomerService` interface- ```go // CustomerServiceServer is the server API for CustomerService service. @@ -102,8 +102,8 @@ func (h *Handler) GetCustomer(ctx context.Context, filter *CustomerFilter) (*Cus } ``` -Lastly to register the gRPC service to the gofr server, user can call the `RegisterCustomerServiceServer` in `customer_grpc.pb.go` -to register the service giving gofr app and the Handler struct. +Lastly to register the gRPC service to the GoFr server, user can call the `RegisterCustomerServiceServer` in `customer_grpc.pb.go` +to register the service giving GoFr app and the Handler struct. ```go package main diff --git a/docs/advanced-guide/handling-data-migrations/page.md b/docs/advanced-guide/handling-data-migrations/page.md index c3fd51894..8b65a3bd9 100644 --- a/docs/advanced-guide/handling-data-migrations/page.md +++ b/docs/advanced-guide/handling-data-migrations/page.md @@ -1,20 +1,20 @@ # Handling Data Migrations Suppose you manually make changes to your database, and now it's your responsibility to inform other developers to execute them. Additionally, you need to keep track of which changes should be applied to production machines in the next deployment. -Gofr supports data migrations for MySQL, Postgres and Redis which allows to alter the state of a database, be it adding a new column to existing table or modifying the data type of existing column or adding constraints to an existing table, setting and removing keys etc. +GoFr supports data migrations for MySQL, Postgres and Redis which allows altering the state of a database, be it adding a new column to existing table or modifying the data type of existing column or adding constraints to an existing table, setting and removing keys etc. ## Usage ### Creating Migration Files -It is recommended to maintain a migrations directory in your project root to enhance readability and maintainability. +It is recommended to maintain a `migrations` directory in your project root to enhance readability and maintainability. **Migration file names** It is recommended that each migration file should be numbered in the format of _YYYYMMDDHHMMSS_ when the migration was created. This helps prevent numbering conflicts and allows for maintaining the correct sort order by name in different filesystem views. -Create the following file in migrations directory. +Create the following file in `migrations` directory. **Filename : 20240226153000_create_employee_table.go** diff --git a/docs/advanced-guide/http-authentication/page.md b/docs/advanced-guide/http-authentication/page.md index 898c393e5..64b064df5 100644 --- a/docs/advanced-guide/http-authentication/page.md +++ b/docs/advanced-guide/http-authentication/page.md @@ -4,7 +4,7 @@ Authentication is a crucial aspect of web applications, controlling access to re It is the process of verifying a user's identity to grant access to protected resources. It ensures that only authenticated users can perform actions or access data within an application. -GoFr offer various approaches to implement authorization. +GoFr offers various approaches to implement authorization. ## 1. HTTP Basic Auth *Basic Authentication* is a simple HTTP authentication scheme where the user's credentials (username and password) are diff --git a/docs/advanced-guide/http-communication/page.md b/docs/advanced-guide/http-communication/page.md index cc2062097..abeb713fe 100644 --- a/docs/advanced-guide/http-communication/page.md +++ b/docs/advanced-guide/http-communication/page.md @@ -19,7 +19,7 @@ Registration of multiple dependent services is quite easier, which is a common u > The services instances are maintained by the container. -Other provided options can be added additionally to coat the basic http client with features like circuit-breaker and +Other provided options can be added additionally to coat the basic HTTP client with features like circuit-breaker and custom health check and add to the functionality of the HTTP service. The design choice for this was made such as many options as required can be added and are order agnostic, i.e. the order of the options is not important. diff --git a/docs/advanced-guide/injecting-databases-drivers/page.md b/docs/advanced-guide/injecting-databases-drivers/page.md index 0a0306298..bcf19a114 100644 --- a/docs/advanced-guide/injecting-databases-drivers/page.md +++ b/docs/advanced-guide/injecting-databases-drivers/page.md @@ -6,9 +6,9 @@ as unnecessary database drivers are not being compiled and added to the build. > We are planning to provide custom drivers for most common databases, and is in the pipeline for upcoming releases! -## Mongo DB -Gofr supports injecting Mongo DB that supports the following interface. Any driver that implements the interface can be added -using `app.UseMongo()` method, and user's can use MongoDB across application with `gofr.Context`. +## MongoDB +GoFr supports injecting MongoDB that supports the following interface. Any driver that implements the interface can be added +using `app.AddMongo()` method, and user's can use MongoDB across application with `gofr.Context`. ```go type Mongo interface { Find(ctx context.Context, collection string, filter interface{}, results interface{}) error @@ -42,7 +42,7 @@ compromising the extensibility to use multiple databases. package main import ( - mongo "github.com/vipul-rawat/gofr-mongo" + "gofr.dev/pkg/gofr/datasource/mongo" "go.mongodb.org/mongo-driver/bson" "gofr.dev/pkg/gofr" @@ -56,13 +56,12 @@ type Person struct { func main() { app := gofr.New() - - // using the mongo driver from `vipul-rawat/gofr-mongo` - db := mongo.New(app.Config, app.Logger(), app.Metrics()) + + db := mongo.New(Config{URI: "mongodb://localhost:27017", Database: "test"}) // inject the mongo into gofr to use mongoDB across the application // using gofr context - app.UseMongo(db) + app.AddMongo(db) app.POST("/mongo", Insert) app.GET("/mongo", Get) diff --git a/docs/advanced-guide/middlewares/page.md b/docs/advanced-guide/middlewares/page.md index abf41e523..994c6d9ca 100644 --- a/docs/advanced-guide/middlewares/page.md +++ b/docs/advanced-guide/middlewares/page.md @@ -1,7 +1,7 @@ # Middleware in GoFr -Middleware allows you to intercept and manipulate HTTP requests and responses flowing through your application's -router. Middlewares can perform tasks such as authentication, authorization, caching etc. before +Middleware allows you intercepting and manipulating HTTP requests and responses flowing through your application's +router. Middlewares can perform tasks such as authentication, authorization, caching etc. before or after the request reaches your application's handler. ## Adding Custom Middleware in GoFr diff --git a/docs/advanced-guide/overriding-default/page.md b/docs/advanced-guide/overriding-default/page.md index 14d07b6de..5c14194c6 100644 --- a/docs/advanced-guide/overriding-default/page.md +++ b/docs/advanced-guide/overriding-default/page.md @@ -1,6 +1,6 @@ # Overriding Default -GoFr allows to override default behavior of its features. +GoFr allows overriding default behavior of its features. ## Raw response format @@ -74,7 +74,7 @@ Response example: ## Favicon.ico -By default GoFr load it's own `favicon.ico` present in root directory for an application. To override `favicon.ico` user -can place it's custom icon in the **static** directory of it's application. +By default GoFr load its own `favicon.ico` present in root directory for an application. To override `favicon.ico` user +can place its custom icon in the **static** directory of its application. > NOTE: The custom favicon should also be named as `favicon.ico` in the static directory of application. \ No newline at end of file diff --git a/docs/advanced-guide/publishing-custom-metrics/page.md b/docs/advanced-guide/publishing-custom-metrics/page.md index d8724c612..33eb14249 100644 --- a/docs/advanced-guide/publishing-custom-metrics/page.md +++ b/docs/advanced-guide/publishing-custom-metrics/page.md @@ -3,7 +3,7 @@ GoFr publishes some {% new-tab-link newtab=false title="default metrics" href="/docs/quick-start/observability" /%}. GoFr can handle multiple different metrics concurrently, each uniquely identified by its name during initialization. -It supports the following {% new-tab-link title="metrics" href="https://opentelemetry.io/docs/specs/otel/metrics/" /%} types in prometheus format: +It supports the following {% new-tab-link title="metrics" href="https://opentelemetry.io/docs/specs/otel/metrics/" /%} types in Prometheus format: 1. Counter 2. UpDownCounter diff --git a/docs/advanced-guide/remote-log-level-change/page.md b/docs/advanced-guide/remote-log-level-change/page.md index 13becf1d2..2191f3ae4 100644 --- a/docs/advanced-guide/remote-log-level-change/page.md +++ b/docs/advanced-guide/remote-log-level-change/page.md @@ -1,6 +1,6 @@ # Remote Log Level Change -Gofr makes it easy to adjust the details captured in the application's logs, even while it's running! +GoFr makes it easy to adjust the details captured in the application's logs, even while it's running! This feature allows users to effortlessly fine-tune logging levels without the need for redeployment, enhancing the monitoring and debugging experience. It is facilitated through simple configuration settings. diff --git a/docs/advanced-guide/using-cron/page.md b/docs/advanced-guide/using-cron/page.md index bbb45d070..4a3d3e659 100644 --- a/docs/advanced-guide/using-cron/page.md +++ b/docs/advanced-guide/using-cron/page.md @@ -13,7 +13,7 @@ What can users automate with cron? Basically, any task that can be expressed as a command or script can be automated with cron. Writing a cron job! -On linux like systems cron jobs can be added by adding a line to the crontab file, specifying the schedule and the command +On Linux like systems cron jobs can be added by adding a line to the crontab file, specifying the schedule and the command that needs to be run at that schedule. The cron schedule is expressed in the following format. `minute hour day_of_month month day_of_week` @@ -22,8 +22,8 @@ Each field can take a specific value or combination of values to define the sche `*` (asterisk) to represent **any** value and `,` (comma) to separate multiple values. It also supports `0-n` to define a range of values for which the cron should run and `*/n` to define number of times the cron should run. Here n is an integer. -## Adding cron jobs in gofr applications -Adding cron jobs to gofr applications is made easy with a simple injection of user's function to the cron table maintained +## Adding cron jobs in GoFr applications +Adding cron jobs to GoFr applications is made easy with a simple injection of user's function to the cron table maintained by the gofr. The minimum time difference between cron job's two consecutive runs is a minute as it is the least significant scheduling time parameter. ```go diff --git a/docs/advanced-guide/using-publisher-subscriber/page.md b/docs/advanced-guide/using-publisher-subscriber/page.md index e9513dec4..bf2fe331f 100644 --- a/docs/advanced-guide/using-publisher-subscriber/page.md +++ b/docs/advanced-guide/using-publisher-subscriber/page.md @@ -27,10 +27,86 @@ that are specific for the type of message broker user wants to use. ### KAFKA #### Configs +{% table %} +- Name +- Description +- Required +- Default +- Example +- Valid format + +--- + +- `PUBSUB_BACKEND` +- Using Apache Kafka as message broker. +- `+` +- +- `KAFKA` +- Not empty string + +--- + +- `PUBSUB_BROKER` +- Address to connect to kafka broker. +- `+` +- +- `localhost:9092` +- Not empty string + +--- + +- `CONSUMER_ID` +- Consumer group id to uniquely identify the consumer group. +- if consuming +- +- `order-consumer` +- Not empty string + +--- + +- `PUBSUB_OFFSET` +- Determines from whence the consumer group should begin consuming when it finds a partition without a committed offset. +- `-` +- `-1` +- `10` +- int + +--- + +- `KAFKA_BATCH_SIZE` +- Limit on how many messages will be buffered before being sent to a partition. +- `-` +- `100` +- `10` +- Positive int + +--- + +- `KAFKA_BATCH_BYTES` +- Limit the maximum size of a request in bytes before being sent to a partition. +- `-` +- `1048576` +- `65536` +- Positive int + +--- + +- `KAFKA_BATCH_TIMEOUT` +- Time limit on how often incomplete message batches will be flushed to Kafka (in milliseconds). +- `-` +- `1000` +- `300` +- Positive int + +{% /table %} + ```dotenv -PUBSUB_BACKEND=KAFKA // using apache kafka as message broker -PUBSUB_BROKER=localhost:9092 // address to connect to kafka broker -CONSUMER_ID=order-consumer // consumer group id to uniquely identify the consumer group +PUBSUB_BACKEND=KAFKA# using apache kafka as message broker +PUBSUB_BROKER=localhost:9092 +CONSUMER_ID=order-consumer +KAFKA_BATCH_SIZE=1000 +KAFKA_BATCH_BYTES=1048576 +KAFKA_BATCH_TIMEOUT=300 ``` #### Docker setup diff --git a/docs/navigation.js b/docs/navigation.js index 383e74891..3d6d3c7f4 100644 --- a/docs/navigation.js +++ b/docs/navigation.js @@ -29,6 +29,7 @@ export const navigation = [ { title: 'Injecting Databases', href: '/docs/advanced-guide/injecting-databases-drivers' }, { title: 'Dealing with Datasources', href: '/docs/advanced-guide/dealing-with-datasources' }, { title: 'Automatic SwaggerUI Rendering', href: '/docs/advanced-guide/swagger-documentation' }, + {title: 'Error Handling',href: '/docs/advanced-guide/gofr-errors'} // { title: 'Dealing with Remote Files', href: '/docs/advanced-guide/remote-files' }, // { title: 'Supporting OAuth', href: '/docs/advanced-guide/oauth' }, // { title: 'Creating a Static File Server', href: '/docs/advanced-guide/static-file-server' }, @@ -39,7 +40,7 @@ export const navigation = [ title: 'References', links: [ { title: 'Context', href: '/docs/references/context' }, - // { title: 'Configuration', href: '/docs/references/configs' }, + { title: 'Configs', href: '/docs/references/configs' }, // { title: 'HTTP Service', href: '/docs/references/http-service' }, // { title: 'Files', href: '/docs/references/files' }, // { title: 'Datastore', href: '/docs/references/datastore' }, diff --git a/docs/public/jaeger-traces.png b/docs/public/jaeger-traces.png new file mode 100644 index 000000000..88d6deb8b Binary files /dev/null and b/docs/public/jaeger-traces.png differ diff --git a/docs/quick-start/configuration/page.md b/docs/quick-start/configuration/page.md index 4e9ed985a..a4ac7c53e 100644 --- a/docs/quick-start/configuration/page.md +++ b/docs/quick-start/configuration/page.md @@ -2,7 +2,7 @@ GoFr simplifies configuration management by reading configuration via environment variables. Application code is decoupled from how configuration is managed as per the {%new-tab-link title="12-factor" href="https://12factor.net/config" %}. -Configs in GoFr can be used to initialise datasources, tracing , setting log levels, changing default http or metrics port. +Configs in GoFr can be used to initialise datasources, tracing , setting log levels, changing default HTTP or metrics port. This abstraction provides a user-friendly interface for configuring user's application without modifying the code itself. To set configs create a `configs` directory in the project's root and add `.env` file. diff --git a/docs/quick-start/connecting-mysql/page.md b/docs/quick-start/connecting-mysql/page.md index d30a382bf..990184cfa 100644 --- a/docs/quick-start/connecting-mysql/page.md +++ b/docs/quick-start/connecting-mysql/page.md @@ -1,6 +1,6 @@ # Connecting MySQL -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 and Postgres) databases based on configuration variables. ## Setup diff --git a/docs/quick-start/introduction/page.md b/docs/quick-start/introduction/page.md index 569de593a..52e9aa807 100644 --- a/docs/quick-start/introduction/page.md +++ b/docs/quick-start/introduction/page.md @@ -81,4 +81,4 @@ GoFr {% new-tab-link newtab=false title="context" href="/docs/references/contex 3. **Starting the server** - When `app.Run()` is called, it configures ,initiates and runs the HTTP server, middlewares. It manages essential features such as routes for health check endpoints, metrics server, favicon etc. It starts the server on the default port 8000. + When `app.Run()` is called, it configures, initiates and runs the HTTP server, middlewares. It manages essential features such as routes for health check endpoints, metrics server, favicon etc. It starts the server on the default port 8000. diff --git a/docs/quick-start/observability/page.md b/docs/quick-start/observability/page.md index aa21ac897..b0f01bd62 100644 --- a/docs/quick-start/observability/page.md +++ b/docs/quick-start/observability/page.md @@ -67,15 +67,21 @@ GoFr publishes metrics to port: _2121_ on _/metrics_ endpoint in prometheus form --- +- app_info +- gauge +- Number of instances running with info of app and framework + +--- + - app_http_response - histogram -- Response time of http requests in seconds +- Response time of HTTP requests in seconds --- - app_http_service_response - histogram -- Response time of http service requests in seconds +- Response time of HTTP service requests in seconds --- @@ -149,17 +155,18 @@ GoFr automatically exports traces for all requests and responses. GoFr uses {% new-tab-link title="OpenTelemetry" href="https://opentelemetry.io/docs/concepts/what-is-opentelemetry/" /%} , a popular tracing framework, to automatically add traces to all requests and responses. -GoFr has support for both zipkin as well as jaeger trace exporters. - **Automatic Correlation ID Propagation:** When a request enters your GoFr application, GoFr automatically generates a correlation-ID `X-Correlation-ID` and adds it to the response headers. This correlation ID is then propagated to all downstream requests. This means that you can track a request as it travels through your distributed system by simply looking at the correlation ID in the request headers. -### Configuration & Usage +### Configuration & Usage: + +GoFr has support for following trace-exporters: +#### 1. [Zipkin](https://zipkin.io/): -To see the traces install zipkin image using the following docker command +To see the traces install zipkin image using the following docker command: ```bash docker run --name gofr-zipkin -p 2005:9411 -d openzipkin/zipkin:latest @@ -181,7 +188,7 @@ DB_NAME=test_db DB_PORT=3306 # tracing configs -TRACE_EXPORTER=zipkin // Supported : zipkin,jaeger +TRACE_EXPORTER=zipkin TRACER_HOST=localhost TRACER_PORT=2005 @@ -189,7 +196,48 @@ LOG_LEVEL=DEBUG ``` > **NOTE:** If the value of `TRACER_PORT` is not -> provided, gofr uses port `9411` by default. +> provided, GoFr uses port `9411` by default. Open {% new-tab-link title="zipkin" href="http://localhost:2005/zipkin/" /%} and search by TraceID (correlationID) to see the trace. -{% figure src="/quick-start-trace.png" alt="Zapin traces" /%} +{% figure src="/quick-start-trace.png" alt="Zipkin traces" /%} + +#### 2. [Jeager](https://www.jaegertracing.io/): + +To see the traces install jaeger image using the following docker command: + +```bash +docker run -d --name jaeger \ + -e COLLECTOR_OTLP_ENABLED=true \ + -p 16686:16686 \ + -p 14317:4317 \ + -p 14318:4318 \ + jaegertracing/all-in-one:1.41 +``` + +Add Jaeger Tracer configs in `.env` file, your .env will be updated to +```dotenv +# ... no change in other env variables + +# tracing configs +TRACE_EXPORTER=jaeger +TRACER_HOST=localhost +TRACER_PORT=14317 +``` + +Open {% new-tab-link title="zipkin" href="http://localhost:16686/trace/" /%} and search by TraceID (correlationID) to see the trace. +{% figure src="/jaeger-tracing.png" alt="Jaeger traces" /%} + +#### 3. [GoFr Tracer](https://tracer.gofr.dev/) + +GoFr tracer is GoFr's own custom trace exporter as well as collector. You can search a trace by its TraceID (correlationID) +in GoFr's own tracer service available anywhere, anytime. + +Add GoFr Tracer configs in `.env` file, your .env will be updated to +```dotenv +# ... no change in other env variables + +# tracing configs +TRACE_EXPORTER=gofr +``` + +Open {% new-tab-link title="gofr-tracer" href="https://tracer.gofr.dev/" /%} and search by TraceID (correlationID) to see the trace. diff --git a/docs/references/configs/page.md b/docs/references/configs/page.md new file mode 100644 index 000000000..32510f02a --- /dev/null +++ b/docs/references/configs/page.md @@ -0,0 +1,254 @@ +# GoFr Configuration Options + +This document lists all the configuration options supported by the Gofr framework. The configurations are grouped by category for better organization. + +## App Configs + +{% table %} + +- Name: APP_NAME +- Description: Name of the application +- Default Value: gofr-app + +--- + +- Name: APP_ENV +- Description: Name of the environment file to use (e.g., stage.env, prod.env, or local.env). + +--- + +- Name: APP_VERSION +- Description: Application version +- Default Value: dev + +--- + +- Name: LOG_LEVEL +- Description: Level of verbosity for application logs. Supported values are **DEBUG, INFO, NOTICE, WARN, ERROR, FATAL** +- Default Value: INFO + +--- + +- Name: REMOTE_LOG_URL +- Description: URL to remotely change the log level + +--- + +- Name: REMOTE_LOG_FETCH_INTERVAL +- Description: Time interval (in seconds) to check for remote log level updates +- Default Value: 15 + +--- + +- Name: METRICS_PORT +- Description: Port on which the application exposes metrics +- Default Value: 2121 + +--- + +- Name: HTTP_PORT +- Description: Port on which the HTTP server listens +- Default Value: 8000 + +--- + +- Name: GRPC_PORT +- Description: Port on which the gRPC server listens +- Default Value: 9000 + +--- + +- Name: TRACE_EXPORTER +- Description: Tracing exporter to use. Supported values: gofr, zipkin, jaeger. +- Default Value: gofr + +--- + +- Name: TRACER_HOST +- Description: Hostname of the tracing collector. Required if TRACE_EXPORTER is set to zipkin or jaeger. + +--- + +- Name: TRACER_PORT +- Description: Port of the tracing collector. Required if TRACE_EXPORTER is set to zipkin or jaeger. +- Default Value: 9411 + +--- + +- Name: CMD_LOGS_FILE +- Description: File to save the logs in case of a CMD application + +{% endtable %} + +## Datasource Configs + +{% table %} + +- Name: PUBSUB_BACKEND +- Description: Pub/Sub message broker backend +- Supported Values: kafka, google, mqtt + +{% endtable %} + +**For Kafka:** + +{% table %} + +- Name: PUBSUB_BROKER +- Description: Comma-separated list of broker addresses +- Default Value: localhost:9092 + +--- + +- Name: PARTITION_SIZE +- Description: Size of each message partition (in bytes) +- Default Value: 0 + +--- + +- Name: PUBSUB_OFFSET +- Description: Offset to start consuming messages from. -1 for earliest, 0 for latest. +- Default Value: -1 + +--- + +- Name: CONSUMER_ID +- Description: Unique identifier for this consumer +- Default Value: gofr-consumer + +{% endtable %} + +**For Google:** + +{% table %} + +- Name: GOOGLE_PROJECT_ID +- Description: ID of the Google Cloud project. Required for Google Pub/Sub. + +--- + +- Name: GOOGLE_SUBSCRIPTION_NAME +- Description: Name of the Google Pub/Sub subscription. Required for Google Pub/Sub. + +{% endtable %} + +**For MQTT:** + +{% table %} + +- Name: MQTT_PORT +- Description: Port of the MQTT broker +- Default Value: 1883 + +--- + +- Name: MQTT_MESSAGE_ORDER +- Description: Enable guaranteed message order +- Default Value: false + +--- + +- Name: MQTT_PROTOCOL +- Description: Communication protocol. Supported values: tcp, ssl. +- Default Value: tcp + +--- + +- Name: MQTT_HOST +- Description: Hostname of the MQTT broker +- Default Value: localhost + +--- + +- Name: MQTT_USER +- Description: Username for the MQTT broker + +--- + +- Name: MQTT_PASSWORD +- Description: Password for the MQTT broker + +--- + +- Name: MQTT_CLIENT_ID_SUFFIX +- Description: Suffix appended to the client ID + +--- + +- Name: MQTT_QOS +- Description: Quality of Service Level + +{% endtable %} + +### Mongo Configs + +{% table %} + +- Name: MONGO_URI +- Description: URI for connecting to the MongoDB server. + +--- + +- Name: MONGO_DATABASE +- Description: Name of the MongoDB database to use. + +{% endtable %} + +### Redis Configs + +{% table %} + +- Name: REDIS_HOST +- Description: Hostname of the Redis server. + +--- + +- Name: REDIS_PORT +- Description: Port of the Redis server. + +{% endtable %} + +### SQL Configs + +{% table %} + +- Name: DB_DIALECT +- Description: Database dialect. Supported values: mysql, postgres + +--- + +- Name: DB_HOST +- Description: Hostname of the database server. + +--- + +- Name: DB_PORT +- Description: Port of the database server. +- Default Value: 3306 + +--- + +- Name: DB_USER +- Description: Username for the database. + +--- + +- Name: DB_PASSWORD +- Description: Password for the database. + +--- + +- Name: DB_NAME +- Description: Name of the database to use. + +{% endtable %} + +## HTTP Configs + +{% table %} + +- Name: REQUEST_TIMEOUT +- Description: Set the request timeouts (in seconds) for HTTP server. +- Default Value: 5 + +{% endtable %} diff --git a/docs/references/context/page.md b/docs/references/context/page.md index 7c8e09948..30dbf2268 100644 --- a/docs/references/context/page.md +++ b/docs/references/context/page.md @@ -2,7 +2,7 @@ GoFr context is an object injected by the GoFr handler. It contains all the request-specific data, for each request-response cycle a new context is created. The request can be either an HTTP request, GRPC call or a message from Pub-Sub. -GoFr Context also embeds the **_container_** which maintains all the dependencies like databases, logger, http service clients, +GoFr Context also embeds the **_container_** which maintains all the dependencies like databases, logger, HTTP service clients, , metrics manager, etc. This reduces the complexity of the application as users don't have to maintain and keep track of all the dependencies by themselves. diff --git a/examples/grpc-server/README.md b/examples/grpc-server/README.md index 3a3172ba2..dc3cbf7e0 100644 --- a/examples/grpc-server/README.md +++ b/examples/grpc-server/README.md @@ -1,6 +1,6 @@ # GRPC Server Example -This GoFr example demonstrates a simple grpc server. +This GoFr example demonstrates a simple gRPC server. ### To run the example use the command below: ```console diff --git a/examples/http-server-using-redis/README.md b/examples/http-server-using-redis/README.md index 021b2c36b..9fff6f90f 100644 --- a/examples/http-server-using-redis/README.md +++ b/examples/http-server-using-redis/README.md @@ -1,10 +1,10 @@ # Redis Example -This GoFr example demonstrates the use of redis as datasource through a simple http server. +This GoFr example demonstrates the use of Redis as datasource through a simple HTTP server. ### To run the example follow the steps below: -- Run the docker image of redis +- Run the docker image of Redis ```console docker run --name gofr-redis -p 2002:6379 -d redis:7.0.5 ``` diff --git a/examples/http-server/README.md b/examples/http-server/README.md index ff0dec72e..d0a458547 100644 --- a/examples/http-server/README.md +++ b/examples/http-server/README.md @@ -1,15 +1,15 @@ # HTTP Server Example -This GoFr example demonstrates a simple http server which supports redis and mysql as datasources. +This GoFr example demonstrates a simple HTTP server which supports Redis and MySQL as datasources. ### To run the example follow the steps below: -- Run the docker image of redis +- Run the docker image of Redis ```console docker run --name gofr-redis -p 2002:6379 -d redis:7.0.5 ``` -- Run the docker image of mysql +- Run the docker image of MySQL ```console docker run --name gofr-mysql -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=test -p 2001:3306 -d mysql:8.0.30 ``` diff --git a/examples/http-server/main.go b/examples/http-server/main.go index 25db0a047..417538bb0 100644 --- a/examples/http-server/main.go +++ b/examples/http-server/main.go @@ -7,7 +7,9 @@ import ( "time" "github.com/redis/go-redis/v9" + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/datasource" ) func main() { @@ -45,7 +47,7 @@ func ErrorHandler(c *gofr.Context) (interface{}, error) { func RedisHandler(c *gofr.Context) (interface{}, error) { val, err := c.Redis.Get(c, "test").Result() if err != nil && err != redis.Nil { // If key is not found, we are not considering this an error and returning "". - return nil, err + return nil, datasource.ErrorDB{Err: err, Message: "error from redis db"} } return val, nil @@ -83,6 +85,9 @@ func TraceHandler(c *gofr.Context) (interface{}, error) { func MysqlHandler(c *gofr.Context) (interface{}, error) { var value int err := c.SQL.QueryRowContext(c, "select 2+2").Scan(&value) + if err != nil { + return nil, datasource.ErrorDB{Err: err, Message: "error from sql db"} + } - return value, err + return value, nil } diff --git a/examples/using-add-rest-handlers/README.md b/examples/using-add-rest-handlers/README.md index 58843af46..f9e989a0f 100644 --- a/examples/using-add-rest-handlers/README.md +++ b/examples/using-add-rest-handlers/README.md @@ -1,10 +1,10 @@ # AddRESTHandlers Example -This GoFr example demonstrates a simple http server with CRUD operations which are created by GoFr using the given struct. +This GoFr example demonstrates a simple HTTP server with CRUD operations which are created by GoFr using the given struct. ### To run the example follow the steps below: -- Run the docker image of mysql +- Run the docker image of MySQL ```console docker run --name gofr-mysql -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=test -p 2001:3306 -d mysql:8.0.30 ``` diff --git a/examples/using-cron-jobs/main.go b/examples/using-cron-jobs/main.go index c6b701d19..e18ac489a 100644 --- a/examples/using-cron-jobs/main.go +++ b/examples/using-cron-jobs/main.go @@ -25,7 +25,7 @@ func main() { // not running the app to close after we have completed the crons runnning // since this is an example the cron will not be running forever - // to run cron forever, users can start the metric server or normal http server + // to run cron forever, users can start the metric server or normal HTTP server // app.Run() } diff --git a/examples/using-custom-metrics/README.md b/examples/using-custom-metrics/README.md index 38b36be50..a45da746e 100644 --- a/examples/using-custom-metrics/README.md +++ b/examples/using-custom-metrics/README.md @@ -1,6 +1,6 @@ # Custom Metrics Example -This GoFr example demonstrates the use of custom metrics through a simple http server that creates and populate metrics. +This GoFr example demonstrates the use of custom metrics through a simple HTTP server that creates and populate metrics. GoFr by default pushes metrics to port `2121` on `/metrics` endpoint. ### To run the example use the command below: diff --git a/examples/using-file-bind/README.md b/examples/using-file-bind/README.md index d08839bf7..6b000bccd 100644 --- a/examples/using-file-bind/README.md +++ b/examples/using-file-bind/README.md @@ -1,7 +1,7 @@ # Using File Bind Example This GoFr example demonstrates the use of context Bind where incoming request has multipart-form data and then binds -it to the fields of the struct. Gofr currently supports zip file type and also binds the more generic multipart.FileHeader +it to the fields of the struct. GoFr currently supports zip file type and also binds the more generic multipart.FileHeader ### Usage ```go diff --git a/examples/using-file-bind/main.go b/examples/using-file-bind/main.go index d49d01185..7ba3ccea9 100644 --- a/examples/using-file-bind/main.go +++ b/examples/using-file-bind/main.go @@ -26,7 +26,7 @@ type Data struct { Compressed file.Zip `file:"upload"` // The FileHeader determines the generic file format that we can get - // from the multipart form that gets parsed by the incoming http request + // from the multipart form that gets parsed by the incoming HTTP request FileHeader *multipart.FileHeader `file:"a"` } diff --git a/examples/using-http-service/readme.md b/examples/using-http-service/readme.md index 33f29ff85..8e6bdcd15 100644 --- a/examples/using-http-service/readme.md +++ b/examples/using-http-service/readme.md @@ -1,9 +1,9 @@ # Http-Service Example -This GoFr example demonstrates an inter-service http communication along with circuit-breaker as well as +This GoFr example demonstrates an inter-service HTTP communication along with circuit-breaker as well as service health config addition. -User can use the `AddHTTPService` method to add an http service and then later get it using `GetHTTPService("service-name")` +User can use the `AddHTTPService` method to add an HTTP service and then later get it using `GetHTTPService("service-name")` ### To run the example follow the below steps: - Make sure your other service and health endpoint is ready and up on the given address. diff --git a/examples/using-migrations/readme.md b/examples/using-migrations/readme.md index 98b91d24d..4137e4a64 100644 --- a/examples/using-migrations/readme.md +++ b/examples/using-migrations/readme.md @@ -1,17 +1,31 @@ # Migrations Example -This GoFr example demonstrates the use of `migrations` through a simple http server using mysql and redis. +This GoFr example demonstrates the use of `migrations` through a simple HTTP server using MySQL, Redis and Kafka. ### To run the example follow the below steps: -- Run the docker image of mysql and redis +- Run the docker image of MySQL, Redis and Kafka ```console 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 kafka-1 -p 9092:9092 \ +-e KAFKA_ENABLE_KRAFT=yes \ +-e KAFKA_CFG_PROCESS_ROLES=broker,controller \ +-e KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER \ +-e KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 \ +-e KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT \ +-e KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 \ +-e KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true \ +-e KAFKA_BROKER_ID=1 \ +-e KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@127.0.0.1:9093 \ +-e ALLOW_PLAINTEXT_LISTENER=yes \ +-e KAFKA_CFG_NODE_ID=1 \ +-v kafka_data:/bitnami \ +bitnami/kafka:3.4 ``` - Now run the example using below command : ```console go run main.go -``` \ No newline at end of file +``` diff --git a/examples/using-publisher/readme.md b/examples/using-publisher/readme.md index 4adbe7643..0157de36a 100644 --- a/examples/using-publisher/readme.md +++ b/examples/using-publisher/readme.md @@ -1,11 +1,11 @@ # Publisher Example -This GoFr example demonstrates a simple Publisher that publishes to the given topic when an http request is made to it's +This GoFr example demonstrates a simple Publisher that publishes to the given topic when an HTTP request is made to it's matching route. ### To run the example follow the below steps: -- Run the docker image of kafka and ensure that your provided topics are created before publishing +- Run the docker image of Kafka and ensure that your provided topics are created before publishing ```console docker run --name kafka-1 -p 9092:9092 \ -e KAFKA_ENABLE_KRAFT=yes \ diff --git a/go.mod b/go.mod index b502ff474..90240dc8c 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 + github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.19.1 github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 github.com/redis/go-redis/v9 v9.5.1 @@ -33,16 +34,17 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.24.0 go.opentelemetry.io/otel/trace v1.24.0 go.uber.org/mock v0.4.0 - golang.org/x/oauth2 v0.19.0 + golang.org/x/oauth2 v0.20.0 golang.org/x/term v0.20.0 - google.golang.org/api v0.177.0 - google.golang.org/grpc v1.63.2 + google.golang.org/api v0.181.0 + google.golang.org/grpc v1.64.0 google.golang.org/protobuf v1.34.1 + modernc.org/sqlite v1.22.1 ) require ( - cloud.google.com/go v0.112.2 // indirect - cloud.google.com/go/auth v0.3.0 // indirect + cloud.google.com/go v0.113.0 // indirect + cloud.google.com/go/auth v0.4.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect cloud.google.com/go/compute/metadata v0.3.0 // indirect cloud.google.com/go/iam v1.1.7 // indirect @@ -53,6 +55,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -61,10 +64,12 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.3 // indirect + github.com/googleapis/gax-go/v2 v2.12.4 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/klauspost/compress v1.16.6 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/klauspost/compress v1.17.8 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/openzipkin/zipkin-go v0.4.2 // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -72,6 +77,7 @@ require ( github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect @@ -80,14 +86,25 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect go.opentelemetry.io/proto/otlp v1.1.0 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/net v0.24.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/mod v0.11.0 // indirect + golang.org/x/net v0.25.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/text v0.15.0 // indirect golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.9.3 // indirect google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/uint128 v1.2.0 // indirect + modernc.org/cc/v3 v3.40.0 // indirect + modernc.org/ccgo/v3 v3.16.13 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/opt v0.1.3 // indirect + modernc.org/strutil v1.1.3 // indirect + modernc.org/token v1.0.1 // indirect ) diff --git a/go.sum b/go.sum index 59c0acffe..6a8d68866 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.112.2 h1:ZaGT6LiG7dBzi6zNOvVZwacaXlmf3lRqnC4DQzqyRQw= -cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms= -cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs= -cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= +cloud.google.com/go v0.113.0 h1:g3C70mn3lWfckKBiCVsAshabrDg01pQ0pnX1MNtnMkA= +cloud.google.com/go v0.113.0/go.mod h1:glEqlogERKYeePz6ZdkcLJ28Q2I6aERgDDErBg9GzO8= +cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg= +cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro= cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= @@ -49,6 +49,8 @@ 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -100,6 +102,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -107,8 +111,8 @@ 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.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= -github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg= +github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI= 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.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= @@ -119,12 +123,14 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk= -github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -135,6 +141,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= @@ -148,6 +158,8 @@ github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFu github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= @@ -166,6 +178,9 @@ github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnA github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/segmentio/kafka-go v0.4.47 h1:IqziR4pA3vrZq7YdRxaT3w1/5fvIH5qpCwstUanQQB0= @@ -240,8 +255,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.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 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= @@ -251,6 +266,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -265,11 +282,11 @@ 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.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= -golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= +golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -291,6 +308,7 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/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.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.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= @@ -309,8 +327,8 @@ 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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -324,12 +342,14 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM= +golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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.177.0 h1:8a0p/BbPa65GlqGWtUKxot4p0TV8OGOfyTjtmkXNXmk= -google.golang.org/api v0.177.0/go.mod h1:srbhue4MLjkjbkux5p3dw/ocYOSZTaIEvf7bCOnFQDw= +google.golang.org/api v0.181.0 h1:rPdjwnWgiPPOJx3IcSAQ2III5aX5tCer6wMpa/xmZi4= +google.golang.org/api v0.181.0/go.mod h1:MnQ+M0CFsfUwA5beZ+g/vCBCPXvtmZwRz2qzZk8ih1k= 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= @@ -338,18 +358,18 @@ google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw= google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw= -google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 h1:DTJM0R8LECCgFeUwApvcEJHz85HLagW8uRENYxHh1ww= -google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6/go.mod h1:10yRODfgim2/T8csjQsMPgZOMvtytXKTDRzH6HRGzRw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 h1:DujSIu+2tC9Ht0aPNA7jgj23Iq8Ewi5sgkQ++wdvonE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae h1:AH34z6WAGVNkllnKs5raNq3yRq93VnjBG6rpfub/jYk= +google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:FfiGhwUm6CJviekPrc0oJ+7h29e+DmWU6UtjX0ZvI7Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 h1:mxSlqyb8ZAHsYDCfiXN1EDdNTdvjUJSLY+OnAUtYNYA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= 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= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -377,3 +397,31 @@ gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.22.1 h1:P2+Dhp5FR1RlVRkQ3dDfCiv3Ok8XPxqpe70IjYVA9oE= +modernc.org/sqlite v1.22.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= +modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c= +modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= +modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= +modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE= diff --git a/pkg/gofr/cmd_test.go b/pkg/gofr/cmd_test.go index e1d5c17fd..7fb3a6e72 100644 --- a/pkg/gofr/cmd_test.go +++ b/pkg/gofr/cmd_test.go @@ -205,7 +205,7 @@ func Test_Run_ErrorNoArgumentGiven(t *testing.T) { assert.Contains(t, logs, "No Command Found!") } -func Test_Run_SuccessCallInvalidHypens(t *testing.T) { +func Test_Run_SuccessCallInvalidHyphens(t *testing.T) { os.Args = []string{"", "log", "-param=value", "-b", "-"} c := cmd{} diff --git a/pkg/gofr/config/godotenv_test.go b/pkg/gofr/config/godotenv_test.go index b2d656065..58f79e7c9 100644 --- a/pkg/gofr/config/godotenv_test.go +++ b/pkg/gofr/config/godotenv_test.go @@ -91,7 +91,7 @@ func Test_EnvSuccess_Local_Override(t *testing.T) { assert.Equal(t, "overloaded_api_key", env.Get("API_KEY"), "TEST Failed.\n godotenv success") } -func Test_EnvFailureWithHypen(t *testing.T) { +func Test_EnvFailureWithHyphen(t *testing.T) { envData := map[string]string{ "KEY-WITH-HYPHEN": "DASH-VALUE", "UNABLE_TO_LOAD": "VALUE", diff --git a/pkg/gofr/container/container.go b/pkg/gofr/container/container.go index b6a2d5ed8..4e7424d67 100644 --- a/pkg/gofr/container/container.go +++ b/pkg/gofr/container/container.go @@ -88,12 +88,18 @@ func (c *Container) Create(conf config.Config) { if conf.Get("PUBSUB_BROKER") != "" { partition, _ := strconv.Atoi(conf.GetOrDefault("PARTITION_SIZE", "0")) offSet, _ := strconv.Atoi(conf.GetOrDefault("PUBSUB_OFFSET", "-1")) + batchSize, _ := strconv.Atoi(conf.GetOrDefault("KAFKA_BATCH_SIZE", strconv.Itoa(kafka.DefaultBatchSize))) + batchBytes, _ := strconv.Atoi(conf.GetOrDefault("KAFKA_BATCH_BYTES", strconv.Itoa(kafka.DefaultBatchBytes))) + batchTimeout, _ := strconv.Atoi(conf.GetOrDefault("KAFKA_BATCH_TIMEOUT", strconv.Itoa(kafka.DefaultBatchTimeout))) c.PubSub = kafka.New(kafka.Config{ Broker: conf.Get("PUBSUB_BROKER"), Partition: partition, ConsumerGroupID: conf.Get("CONSUMER_ID"), OffSet: offSet, + BatchSize: batchSize, + BatchBytes: batchBytes, + BatchTimeout: batchTimeout, }, c.Logger, c.metricsManager) } case "GOOGLE": @@ -131,8 +137,8 @@ func (c *Container) Create(conf config.Config) { } } -// GetHTTPService returns registered http services. -// HTTP services are registered from AddHTTPService method of gofr object. +// GetHTTPService returns registered HTTP services. +// HTTP services are registered from AddHTTPService method of GoFr object. func (c *Container) GetHTTPService(serviceName string) service.HTTP { return c.Services[serviceName] } @@ -150,20 +156,23 @@ func (c *Container) registerFrameworkMetrics() { c.Metrics().NewGauge("app_go_numGC", "Number of completed Garbage Collector cycles.") c.Metrics().NewGauge("app_go_sys", "Number of total bytes of memory.") - // http metrics - httpBuckets := []float64{.001, .003, .005, .01, .02, .03, .05, .1, .2, .3, .5, .75, 1, 2, 3, 5, 10, 30} - c.Metrics().NewHistogram("app_http_response", "Response time of http requests in seconds.", httpBuckets...) - c.Metrics().NewHistogram("app_http_service_response", "Response time of http service requests in seconds.", httpBuckets...) + { // HTTP metrics + httpBuckets := []float64{.001, .003, .005, .01, .02, .03, .05, .1, .2, .3, .5, .75, 1, 2, 3, 5, 10, 30} + c.Metrics().NewHistogram("app_http_response", "Response time of HTTP requests in seconds.", httpBuckets...) + c.Metrics().NewHistogram("app_http_service_response", "Response time of HTTP service requests in seconds.", httpBuckets...) + } - // redis metrics - redisBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 1.25, 1.5, 2, 2.5, 3} - c.Metrics().NewHistogram("app_redis_stats", "Response time of Redis commands in milliseconds.", redisBuckets...) + { // Redis metrics + redisBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 1.25, 1.5, 2, 2.5, 3} + c.Metrics().NewHistogram("app_redis_stats", "Response time of Redis commands in milliseconds.", redisBuckets...) + } - // sql metrics - sqlBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10} - c.Metrics().NewHistogram("app_sql_stats", "Response time of SQL queries in milliseconds.", sqlBuckets...) - c.Metrics().NewGauge("app_sql_open_connections", "Number of open SQL connections.") - c.Metrics().NewGauge("app_sql_inUse_connections", "Number of inUse SQL connections.") + { // SQL metrics + sqlBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10} + c.Metrics().NewHistogram("app_sql_stats", "Response time of SQL queries in milliseconds.", sqlBuckets...) + c.Metrics().NewGauge("app_sql_open_connections", "Number of open SQL connections.") + c.Metrics().NewGauge("app_sql_inUse_connections", "Number of inUse SQL connections.") + } // pubsub metrics c.Metrics().NewCounter("app_pubsub_publish_total_count", "Number of total publish operations.") diff --git a/pkg/gofr/context.go b/pkg/gofr/context.go index fe514b111..ab8a26664 100644 --- a/pkg/gofr/context.go +++ b/pkg/gofr/context.go @@ -22,7 +22,7 @@ type Context struct { // responder is private as Handlers do not need to worry about how to respond. But it is still an abstraction over // normal response writer as we want to keep the context independent of http. Will help us in writing CMD application - // or grpc servers etc using the same handler signature. + // or gRPC servers etc using the same handler signature. responder Responder } diff --git a/pkg/gofr/crud_handlers_test.go b/pkg/gofr/crud_handlers_test.go index 23a0748b8..39ee18dd2 100644 --- a/pkg/gofr/crud_handlers_test.go +++ b/pkg/gofr/crud_handlers_test.go @@ -139,7 +139,8 @@ func Test_CreateHandler(t *testing.T) { ctx := createTestContext(http.MethodPost, "/users", "", tc.reqBody, c) - mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", gomock.Any(), "type", "INSERT").MaxTimes(2) + mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", gomock.Any(), + "hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT").MaxTimes(2) if tc.expectedErr == nil { mocks.SQL.EXPECT().Dialect().Return(tc.dialect).Times(1) @@ -325,7 +326,8 @@ func Test_GetHandler(t *testing.T) { ctx := createTestContext(http.MethodGet, "/user", tc.id, nil, c) - mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", gomock.Any(), "type", "SELECT") + mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", gomock.Any(), + "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT") mock.ExpectQuery(dc.expectedQuery).WithArgs(tc.id).WillReturnRows(tc.mockRow).WillReturnError(tc.mockErr) resp, err := e.Get(ctx) @@ -416,7 +418,7 @@ func Test_UpdateHandler(t *testing.T) { ctx := createTestContext(http.MethodPut, "/user", strconv.Itoa(tc.id), tc.reqBody, c) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", gomock.Any(), - "type", "UPDATE").MaxTimes(2) + "hostname", gomock.Any(), "database", gomock.Any(), "type", "UPDATE").MaxTimes(2) mock.ExpectExec(dc.expectedQuery).WithArgs("goFr", true, tc.id). WillReturnResult(sqlmock.NewResult(1, 1)).WillReturnError(nil) diff --git a/pkg/gofr/datasource/README.md b/pkg/gofr/datasource/README.md index d186e305c..f9914b723 100644 --- a/pkg/gofr/datasource/README.md +++ b/pkg/gofr/datasource/README.md @@ -46,7 +46,7 @@ All logs should include: ## Implementing New Datasources -GoFr offers built-in support for popular datasources like SQL (MySQL, PostgreSQL), Redis, and Pub/Sub (MQTT, Kafka, Google as backend). Including additional functionalities within the core GoFr binary would increase the application size unnecessarily. +GoFr offers built-in support for popular datasources like SQL (MySQL, PostgreSQL, SQLite), Redis, and Pub/Sub (MQTT, Kafka, Google as backend). Including additional functionalities within the core GoFr binary would increase the application size unnecessarily. Therefore, GoFr utilizes a pluggable approach for new datasources by separating implementation in the following way: @@ -73,6 +73,6 @@ Therefore, GoFr utilizes a pluggable approach for new datasources by separating | MySQL | ✅ | ✅ | ✅ | ✅ | | | REDIS | ✅ | ✅ | ✅ | ✅ | | | PostgreSQL | ✅ | ✅ | ✅ | ✅ | | -| MongoDB | | | | | ✅ | +| MongoDB | ✅ | ✅ | ✅ | | ✅ | +| SQLite | ✅ | ✅ | ✅ | ✅ | | -> Following list has to be update when PR for new Datasource is being created. \ No newline at end of file diff --git a/pkg/gofr/datasource/errors.go b/pkg/gofr/datasource/errors.go new file mode 100644 index 000000000..26fd02d67 --- /dev/null +++ b/pkg/gofr/datasource/errors.go @@ -0,0 +1,34 @@ +package datasource + +import ( + "net/http" + + "github.com/pkg/errors" +) + +// ErrorDB represents an error specific to database operations. +type ErrorDB struct { + Err error + Message string +} + +func (e ErrorDB) Error() string { + switch { + case e.Message == "": + return e.Err.Error() + case e.Err == nil: + return e.Message + default: + return errors.Wrap(e.Err, e.Message).Error() + } +} + +// WithStack adds a stack trace to the Error. +func (e ErrorDB) WithStack() ErrorDB { + e.Err = errors.WithStack(e.Err) + return e +} + +func (e ErrorDB) StatusCode() int { + return http.StatusInternalServerError +} diff --git a/pkg/gofr/datasource/errors_test.go b/pkg/gofr/datasource/errors_test.go new file mode 100644 index 000000000..b0b333f64 --- /dev/null +++ b/pkg/gofr/datasource/errors_test.go @@ -0,0 +1,35 @@ +package datasource + +import ( + "net/http" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func Test_ErrorDB(t *testing.T) { + wrappedErr := errors.New("underlying error") + + tests := []struct { + desc string + err ErrorDB + expectedMsg string + }{ + {"wrapped error", ErrorDB{Err: wrappedErr, Message: "custom message"}.WithStack(), "custom message: underlying error"}, + {"without wrapped error", ErrorDB{Message: "custom message"}, "custom message"}, + {"no custom error message", ErrorDB{Err: wrappedErr}, "underlying error"}, + } + + for i, tc := range tests { + assert.Equal(t, tc.err.Error(), tc.expectedMsg, "TEST[%d], Failed.\n%s", i, tc.desc) + } +} + +func TestErrorDB_StatusCode(t *testing.T) { + dbErr := ErrorDB{Message: "custom message"} + + expectedCode := http.StatusInternalServerError + + assert.Equal(t, expectedCode, dbErr.StatusCode(), "TEST Failed.\n") +} diff --git a/pkg/gofr/datasource/mongo.go b/pkg/gofr/datasource/mongo.go index a88bb8195..7948a05f8 100644 --- a/pkg/gofr/datasource/mongo.go +++ b/pkg/gofr/datasource/mongo.go @@ -50,3 +50,18 @@ type Mongo interface { // It returns an error if any. Drop(ctx context.Context, collection string) error } + +// MongoProvider is an interface that extends Mongo with additional methods for logging, metrics, and connection management. +// Which is used for initializing datasource. +type MongoProvider interface { + Mongo + + // UseLogger sets the logger for the MongoDB client. + UseLogger(logger interface{}) + + // UseMetrics sets the metrics for the MongoDB client. + UseMetrics(metrics interface{}) + + // Connect establishes a connection to MongoDB and registers metrics using the provided configuration when the client was Created. + Connect() +} diff --git a/pkg/gofr/datasource/mongo/go.mod b/pkg/gofr/datasource/mongo/go.mod new file mode 100644 index 000000000..abe218033 --- /dev/null +++ b/pkg/gofr/datasource/mongo/go.mod @@ -0,0 +1,27 @@ +module gofr.dev/pkg/gofr/datasource/mongo + +go 1.22 + +toolchain go1.22.3 + +require ( + github.com/stretchr/testify v1.9.0 + go.mongodb.org/mongo-driver v1.15.0 + go.uber.org/mock v0.4.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/klauspost/compress v1.17.8 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/text v0.15.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/pkg/gofr/datasource/mongo/go.sum b/pkg/gofr/datasource/mongo/go.sum new file mode 100644 index 000000000..ca16041a8 --- /dev/null +++ b/pkg/gofr/datasource/mongo/go.sum @@ -0,0 +1,62 @@ +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/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +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= +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= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 h1:tBiBTKHnIjovYoLX/TPkcf+OjqqKGQrPtGT3Foz+Pgo= +github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76/go.mod h1:SQliXeA7Dhkt//vS29v3zpbEwoa+zb2Cn5xj5uO4K5U= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= +go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +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.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +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= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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/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/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= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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/mongo/logger.go b/pkg/gofr/datasource/mongo/logger.go new file mode 100644 index 000000000..9d955878b --- /dev/null +++ b/pkg/gofr/datasource/mongo/logger.go @@ -0,0 +1,55 @@ +package mongo + +import ( + "fmt" + "io" + "regexp" + "strings" +) + +type Logger interface { + Debugf(pattern string, args ...interface{}) + Logf(pattern string, args ...interface{}) + Errorf(patter string, args ...interface{}) +} + +type QueryLog struct { + Query string `json:"query"` + Duration int64 `json:"duration"` + Collection string `json:"collection,omitempty"` + Filter interface{} `json:"filter,omitempty"` + ID interface{} `json:"id,omitempty"` + Update interface{} `json:"update,omitempty"` +} + +func (ql *QueryLog) PrettyPrint(writer io.Writer) { + if ql.Filter == nil { + ql.Filter = "" + } + + if ql.ID == nil { + ql.ID = "" + } + + if ql.Update == nil { + ql.Update = "" + } + + fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;206m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s\n", + clean(ql.Query), "MONGO", ql.Duration, + clean(strings.Join([]string{ql.Collection, fmt.Sprint(ql.Filter), fmt.Sprint(ql.ID), fmt.Sprint(ql.Update)}, " "))) +} + +// clean takes a string query as input and performs two operations to clean it up: +// 1. It replaces multiple consecutive whitespace characters with a single space. +// 2. It trims leading and trailing whitespace from the string. +// The cleaned-up query string is then returned. +func clean(query string) string { + // Replace multiple consecutive whitespace characters with a single space + query = regexp.MustCompile(`\s+`).ReplaceAllString(query, " ") + + // Trim leading and trailing whitespace from the string + query = strings.TrimSpace(query) + + return query +} diff --git a/pkg/gofr/datasource/mongo/logger_test.go b/pkg/gofr/datasource/mongo/logger_test.go new file mode 100644 index 000000000..c9381b760 --- /dev/null +++ b/pkg/gofr/datasource/mongo/logger_test.go @@ -0,0 +1,38 @@ +package mongo + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoggingDataPresent(t *testing.T) { + queryLog := QueryLog{ + Query: "find", + Duration: 12345, + Collection: "users", + Filter: map[string]string{"name": "John"}, + ID: "123", + Update: map[string]string{"$set": "Doe"}, + } + expected := "name:John" + + var buf bytes.Buffer + queryLog.PrettyPrint(&buf) + + assert.Contains(t, buf.String(), expected) +} + +func TestLoggingEmptyData(t *testing.T) { + queryLog := QueryLog{ + Query: "insert", + Duration: 6789, + } + expected := "name:John" + + var buf bytes.Buffer + queryLog.PrettyPrint(&buf) + + assert.NotContains(t, buf.String(), expected) +} diff --git a/pkg/gofr/datasource/mongo/metrics.go b/pkg/gofr/datasource/mongo/metrics.go new file mode 100644 index 000000000..f1ff591fd --- /dev/null +++ b/pkg/gofr/datasource/mongo/metrics.go @@ -0,0 +1,9 @@ +package mongo + +import "context" + +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/mongo/mock_logger.go b/pkg/gofr/datasource/mongo/mock_logger.go new file mode 100644 index 000000000..1c44c1002 --- /dev/null +++ b/pkg/gofr/datasource/mongo/mock_logger.go @@ -0,0 +1,53 @@ +package mongo + +import ( + "fmt" + "io" + "os" +) + +// Level represents different logging levels. +type Level int + +const ( + DEBUG Level = iota + 1 + INFO + ERROR +) + +type MockLogger struct { + level Level + out io.Writer + errOut io.Writer +} + +func NewMockLogger(level Level) Logger { + return &MockLogger{ + level: level, + out: os.Stdout, + errOut: os.Stderr, + } +} + +func (m *MockLogger) Debugf(pattern string, args ...interface{}) { + m.logf(DEBUG, pattern, args...) +} + +func (m *MockLogger) Logf(pattern string, args ...interface{}) { + m.logf(INFO, pattern, args...) +} + +func (m *MockLogger) Errorf(patter string, args ...interface{}) { + m.logf(ERROR, patter, args...) +} + +func (m *MockLogger) logf(level Level, format string, args ...interface{}) { + out := m.out + if level == ERROR { + out = m.errOut + } + + message := fmt.Sprintf(format, args...) + + fmt.Fprintf(out, "%v\n", message) +} diff --git a/pkg/gofr/datasource/mongo/mock_metrics.go b/pkg/gofr/datasource/mongo/mock_metrics.go new file mode 100644 index 000000000..fb40648ce --- /dev/null +++ b/pkg/gofr/datasource/mongo/mock_metrics.go @@ -0,0 +1,74 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: metrics.go +// +// Generated by this command: +// +// mockgen -source=metrics.go -destination=mock_metrics.go -package=mongo +// + +// Package mongo is a generated GoMock package. +package mongo + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// 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/mongo/mongo.go b/pkg/gofr/datasource/mongo/mongo.go new file mode 100644 index 000000000..cda868be6 --- /dev/null +++ b/pkg/gofr/datasource/mongo/mongo.go @@ -0,0 +1,228 @@ +package mongo + +import ( + "context" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/mongo/readpref" +) + +type Client struct { + *mongo.Database + + uri string + database string + logger Logger + metrics Metrics + config Config +} + +type Config struct { + URI string + Database string +} + +/* +Developer Note: We could have accepted logger and metrics as part of the factory function `New`, but when mongo driver is +initialised in GoFr, We want to ensure that the user need not to provides logger and metrics and then connect to the database, +i.e. by default observability features gets initialised when used with GoFr. +*/ + +// New initializes MongoDB driver with the provided configuration. +// The Connect method must be called to establish a connection to MongoDB. +// Usage: +// client := New(config) +// client.UseLogger(loggerInstance) +// client.UseMetrics(metricsInstance) +// client.Connect() +func New(c Config) *Client { + return &Client{config: c} +} + +// UseLogger sets the logger for the MongoDB client which asserts the Logger interface. +func (c *Client) UseLogger(logger interface{}) { + if l, ok := logger.(Logger); ok { + c.logger = l + } +} + +// UseMetrics sets the metrics for the MongoDB client which asserts the Metrics interface. +func (c *Client) UseMetrics(metrics interface{}) { + if m, ok := metrics.(Metrics); ok { + c.metrics = m + } +} + +// 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) + + m, err := mongo.Connect(context.Background(), options.Client().ApplyURI(c.config.URI)) + if err != nil { + c.logger.Errorf("error connecting to mongoDB, err:%v", err) + + return + } + + 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) +} + +// InsertOne inserts a single document into the specified collection. +func (c *Client) InsertOne(ctx context.Context, collection string, document interface{}) (interface{}, error) { + defer c.postProcess(&QueryLog{Query: "insertOne", Collection: collection, Filter: document}, time.Now()) + + return c.Database.Collection(collection).InsertOne(ctx, document) +} + +// InsertMany inserts multiple documents into the specified collection. +func (c *Client) InsertMany(ctx context.Context, collection string, documents []interface{}) ([]interface{}, error) { + defer c.postProcess(&QueryLog{Query: "insertMany", Collection: collection, Filter: documents}, time.Now()) + + res, err := c.Database.Collection(collection).InsertMany(ctx, documents) + if err != nil { + return nil, err + } + + return res.InsertedIDs, nil +} + +// Find retrieves documents from the specified collection based on the provided filter and binds response to result. +func (c *Client) Find(ctx context.Context, collection string, filter, results interface{}) error { + defer c.postProcess(&QueryLog{Query: "find", Collection: collection, Filter: filter}, time.Now()) + + cur, err := c.Database.Collection(collection).Find(ctx, filter) + if err != nil { + return err + } + + defer cur.Close(ctx) + + if err := cur.All(ctx, results); err != nil { + return err + } + + return nil +} + +// FindOne retrieves a single document from the specified collection based on the provided filter and binds response to result. +func (c *Client) FindOne(ctx context.Context, collection string, filter, result interface{}) error { + defer c.postProcess(&QueryLog{Query: "findOne", Collection: collection, Filter: filter}, time.Now()) + + b, err := c.Database.Collection(collection).FindOne(ctx, filter).Raw() + if err != nil { + return err + } + + return bson.Unmarshal(b, result) +} + +// UpdateByID updates a document in the specified collection by its ID. +func (c *Client) UpdateByID(ctx context.Context, collection string, id, update interface{}) (int64, error) { + defer c.postProcess(&QueryLog{Query: "updateByID", Collection: collection, ID: id, Update: update}, time.Now()) + + res, err := c.Database.Collection(collection).UpdateByID(ctx, id, update) + + return res.ModifiedCount, err +} + +// UpdateOne updates a single document in the specified collection based on the provided filter. +func (c *Client) UpdateOne(ctx context.Context, collection string, filter, update interface{}) error { + defer c.postProcess(&QueryLog{Query: "updateOne", Collection: collection, Filter: filter, Update: update}, time.Now()) + + _, err := c.Database.Collection(collection).UpdateOne(ctx, filter, update) + + return err +} + +// UpdateMany updates multiple documents in the specified collection based on the provided filter. +func (c *Client) UpdateMany(ctx context.Context, collection string, filter, update interface{}) (int64, error) { + defer c.postProcess(&QueryLog{Query: "updateMany", Collection: collection, Filter: filter, Update: update}, time.Now()) + + res, err := c.Database.Collection(collection).UpdateMany(ctx, filter, update) + + return res.ModifiedCount, err +} + +// CountDocuments counts the number of documents in the specified collection based on the provided filter. +func (c *Client) CountDocuments(ctx context.Context, collection string, filter interface{}) (int64, error) { + defer c.postProcess(&QueryLog{Query: "countDocuments", Collection: collection, Filter: filter}, time.Now()) + + return c.Database.Collection(collection).CountDocuments(ctx, filter) +} + +// DeleteOne deletes a single document from the specified collection based on the provided filter. +func (c *Client) DeleteOne(ctx context.Context, collection string, filter interface{}) (int64, error) { + defer c.postProcess(&QueryLog{Query: "deleteOne", Collection: collection, Filter: filter}, time.Now()) + + res, err := c.Database.Collection(collection).DeleteOne(ctx, filter) + if err != nil { + return 0, err + } + + return res.DeletedCount, nil +} + +// DeleteMany deletes multiple documents from the specified collection based on the provided filter. +func (c *Client) DeleteMany(ctx context.Context, collection string, filter interface{}) (int64, error) { + defer c.postProcess(&QueryLog{Query: "deleteMany", Collection: collection, Filter: filter}, time.Now()) + + res, err := c.Database.Collection(collection).DeleteMany(ctx, filter) + if err != nil { + return 0, err + } + + return res.DeletedCount, nil +} + +// Drop drops the specified collection from the database. +func (c *Client) Drop(ctx context.Context, collection string) error { + defer c.postProcess(&QueryLog{Query: "drop", Collection: collection}, time.Now()) + + return c.Database.Collection(collection).Drop(ctx) +} + +func (c *Client) postProcess(ql *QueryLog, startTime time.Time) { + duration := time.Since(startTime).Milliseconds() + + ql.Duration = duration + + c.logger.Debugf("%v", ql) + + c.metrics.RecordHistogram(context.Background(), "app_mongo_stats", float64(duration), "hostname", c.uri, + "database", c.database, "type", ql.Query) +} + +type Health struct { + Status string `json:"status,omitempty"` + Details map[string]interface{} `json:"details,omitempty"` +} + +// HealthCheck checks the health of the MongoDB client by pinging the database. +func (c *Client) HealthCheck() interface{} { + h := Health{ + Details: make(map[string]interface{}), + } + + h.Details["host"] = c.uri + h.Details["database"] = c.database + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + err := c.Database.Client().Ping(ctx, readpref.Primary()) + if err != nil { + h.Status = "DOWN" + + return &h + } + + h.Status = "UP" + + return &h +} diff --git a/pkg/gofr/datasource/mongo/mongo_test.go b/pkg/gofr/datasource/mongo/mongo_test.go new file mode 100644 index 000000000..a19311cd4 --- /dev/null +++ b/pkg/gofr/datasource/mongo/mongo_test.go @@ -0,0 +1,436 @@ +package mongo + +import ( + "context" + "fmt" + + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/integration/mtest" + "go.uber.org/mock/gomock" +) + +func Test_NewMongoClient(t *testing.T) { + metrics := NewMockMetrics(gomock.NewController(t)) + + metrics.EXPECT().NewHistogram("app_mongo_stats", "Response time of MONGO queries in milliseconds.", gomock.Any()) + + client := New(Config{URI: "mongodb://localhost:27017", Database: "test"}) + client.UseLogger(NewMockLogger(DEBUG)) + client.UseMetrics(metrics) + client.Connect() + + assert.NotNil(t, client) +} + +func Test_NewMongoClientError(t *testing.T) { + metrics := NewMockMetrics(gomock.NewController(t)) + + client := New(Config{URI: "mongo", Database: "test"}) + client.UseLogger(NewMockLogger(DEBUG)) + client.UseMetrics(metrics) + client.Connect() + + assert.Nil(t, client.Database) +} + +func Test_InsertCommands(t *testing.T) { + // Create a connected client using the mock database + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) + + metrics := NewMockMetrics(gomock.NewController(t)) + + cl := Client{metrics: metrics} + + metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", + gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(4) + + cl.logger = NewMockLogger(DEBUG) + + mt.Run("insertOneSuccess", func(mt *mtest.T) { + cl.Database = mt.DB + mt.AddMockResponses(mtest.CreateSuccessResponse()) + + doc := map[string]interface{}{"name": "Aryan"} + + resp, err := cl.InsertOne(context.Background(), mt.Coll.Name(), doc) + + assert.NotNil(t, resp) + assert.Nil(t, err) + }) + + mt.Run("insertOneError", func(mt *mtest.T) { + cl.Database = mt.DB + mt.AddMockResponses(mtest.CreateWriteErrorsResponse(mtest.WriteError{ + Index: 1, + Code: 11000, + Message: "duplicate key error", + })) + + doc := map[string]interface{}{"name": "Aryan"} + + resp, err := cl.InsertOne(context.Background(), mt.Coll.Name(), doc) + + assert.Nil(t, resp) + assert.NotNil(t, err) + }) + + mt.Run("insertManySuccess", func(mt *mtest.T) { + cl.Database = mt.DB + mt.AddMockResponses(mtest.CreateSuccessResponse()) + + doc := map[string]interface{}{"name": "Aryan"} + + resp, err := cl.InsertMany(context.Background(), mt.Coll.Name(), []interface{}{doc, doc}) + + assert.NotNil(t, resp) + assert.Nil(t, err) + }) + + mt.Run("insertManyError", func(mt *mtest.T) { + cl.Database = mt.DB + mt.AddMockResponses(mtest.CreateWriteErrorsResponse(mtest.WriteError{ + Index: 1, + Code: 11000, + Message: "duplicate key error", + })) + + doc := map[string]interface{}{"name": "Aryan"} + + resp, err := cl.InsertMany(context.Background(), mt.Coll.Name(), []interface{}{doc, doc}) + + assert.Nil(t, resp) + assert.NotNil(t, err) + }) +} + +func Test_FindMultipleCommands(t *testing.T) { + // Create a connected client using the mock database + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) + + metrics := NewMockMetrics(gomock.NewController(t)) + + cl := Client{metrics: metrics} + + metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", + gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(3) + + cl.logger = NewMockLogger(DEBUG) + + mt.Run("FindSuccess", func(mt *mtest.T) { + cl.Database = mt.DB + + var foundDocuments []interface{} + + id1 := primitive.NewObjectID() + + first := mtest.CreateCursorResponse(1, "foo.bar", mtest.FirstBatch, bson.D{ + {Key: "_id", Value: id1}, + {Key: "name", Value: "john"}, + {Key: "email", Value: "john.doe@test.com"}, + }) + + killCursors := mtest.CreateCursorResponse(0, "foo.bar", mtest.NextBatch) + mt.AddMockResponses(first, killCursors) + + mt.AddMockResponses(first) + + err := cl.Find(context.Background(), mt.Coll.Name(), bson.D{{}}, &foundDocuments) + + assert.Nil(t, err, "Unexpected error during Find operation") + }) + + mt.Run("FindCursorError", func(mt *mtest.T) { + cl.Database = mt.DB + mt.AddMockResponses(mtest.CreateSuccessResponse()) + + err := cl.Find(context.Background(), mt.Coll.Name(), bson.D{{}}, nil) + + assert.Equal(t, "database response does not contain a cursor", err.Error()) + }) + + mt.Run("FindCursorParseError", func(mt *mtest.T) { + cl.Database = mt.DB + + var foundDocuments []interface{} + + id1 := primitive.NewObjectID() + + first := mtest.CreateCursorResponse(1, "foo.bar", mtest.FirstBatch, bson.D{ + {Key: "_id", Value: id1}, + {Key: "name", Value: "john"}, + {Key: "email", Value: "john.doe@test.com"}, + }) + + mt.AddMockResponses(first) + + mt.AddMockResponses(first) + + err := cl.Find(context.Background(), mt.Coll.Name(), bson.D{{}}, &foundDocuments) + + assert.Equal(t, "cursor.nextBatch should be an array but is a BSON invalid", err.Error()) + }) +} + +func Test_FindOneCommands(t *testing.T) { + // Create a connected client using the mock database + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) + + metrics := NewMockMetrics(gomock.NewController(t)) + + cl := Client{metrics: metrics} + + metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", + gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(2) + + cl.logger = NewMockLogger(DEBUG) + + mt.Run("FindOneSuccess", func(mt *mtest.T) { + cl.Database = mt.DB + + type user struct { + ID primitive.ObjectID + Name string + Email string + } + + var foundDocuments user + + expectedUser := user{ + ID: primitive.NewObjectID(), + Name: "john", + Email: "john.doe@test.com", + } + + mt.AddMockResponses(mtest.CreateCursorResponse(1, "foo.bar", mtest.FirstBatch, bson.D{ + {Key: "_id", Value: expectedUser.ID}, + {Key: "name", Value: expectedUser.Name}, + {Key: "email", Value: expectedUser.Email}, + })) + + err := cl.FindOne(context.Background(), mt.Coll.Name(), bson.D{{}}, &foundDocuments) + + assert.Equal(t, expectedUser.Name, foundDocuments.Name) + assert.Nil(t, err) + }) + + mt.Run("FindOneError", func(mt *mtest.T) { + cl.Database = mt.DB + + type user struct { + ID primitive.ObjectID + Name string + Email string + } + + var foundDocuments user + + mt.AddMockResponses(mtest.CreateCursorResponse(1, "foo.bar", mtest.FirstBatch)) + + err := cl.FindOne(context.Background(), mt.Coll.Name(), bson.D{{}}, &foundDocuments) + + assert.NotNil(t, err) + }) +} + +func Test_UpdateCommands(t *testing.T) { + // Create a connected client using the mock database + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) + + metrics := NewMockMetrics(gomock.NewController(t)) + + cl := Client{metrics: metrics} + + metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", + gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(3) + + cl.logger = NewMockLogger(DEBUG) + + mt.Run("updateByID", func(mt *mtest.T) { + cl.Database = mt.DB + mt.AddMockResponses(mtest.CreateSuccessResponse()) + // Create a document to insert + + resp, err := cl.UpdateByID(context.Background(), mt.Coll.Name(), "1", bson.M{"$set": bson.M{"name": "test"}}) + + assert.NotNil(t, resp) + assert.Nil(t, err) + }) + + mt.Run("updateOne", func(mt *mtest.T) { + cl.Database = mt.DB + mt.AddMockResponses(mtest.CreateSuccessResponse()) + // Create a document to insert + + err := cl.UpdateOne(context.Background(), mt.Coll.Name(), bson.D{{Key: "name", Value: "test"}}, bson.M{"$set": bson.M{"name": "testing"}}) + + assert.Nil(t, err) + }) + + mt.Run("updateMany", func(mt *mtest.T) { + cl.Database = mt.DB + mt.AddMockResponses(mtest.CreateSuccessResponse()) + // Create a document to insert + + _, err := cl.UpdateMany(context.Background(), mt.Coll.Name(), bson.D{{Key: "name", Value: "test"}}, + bson.M{"$set": bson.M{"name": "testing"}}) + + assert.Nil(t, err) + }) +} + +func Test_CountDocuments(t *testing.T) { + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) + + metrics := NewMockMetrics(gomock.NewController(t)) + + cl := Client{metrics: metrics} + + metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", + gomock.Any(), "database", gomock.Any(), "type", gomock.Any()) + + cl.logger = NewMockLogger(DEBUG) + + mt.Run("countDocuments", func(mt *mtest.T) { + cl.Database = mt.DB + + mt.AddMockResponses(mtest.CreateSuccessResponse()) + + mt.AddMockResponses(mtest.CreateCursorResponse(1, "test.restaurants", mtest.FirstBatch, bson.D{{Key: "n", Value: 1}})) + + // For count to work, mongo needs an index. So we need to create that. Index view should contain a key. Value does not matter + indexView := mt.Coll.Indexes() + _, err := indexView.CreateOne(context.Background(), mongo.IndexModel{ + Keys: bson.D{{Key: "x", Value: 1}}, + }) + require.Nil(mt, err, "CreateOne error for index: %v", err) + + resp, err := cl.CountDocuments(context.Background(), mt.Coll.Name(), bson.D{{Key: "name", Value: "test"}}) + + assert.Equal(t, int64(1), resp) + assert.Nil(t, err) + }) +} + +func Test_DeleteCommands(t *testing.T) { + // Create a connected client using the mock database + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) + + metrics := NewMockMetrics(gomock.NewController(t)) + + cl := Client{metrics: metrics} + + metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", + gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(4) + + cl.logger = NewMockLogger(DEBUG) + + mt.Run("DeleteOne", func(mt *mtest.T) { + cl.Database = mt.DB + mt.AddMockResponses(mtest.CreateSuccessResponse()) + + resp, err := cl.DeleteOne(context.Background(), mt.Coll.Name(), bson.D{{}}) + + assert.Equal(t, int64(0), resp) + assert.Nil(t, err) + }) + + mt.Run("DeleteOneError", func(mt *mtest.T) { + cl.Database = mt.DB + mt.AddMockResponses(mtest.CreateWriteErrorsResponse(mtest.WriteError{ + Index: 1, + Code: 11000, + Message: "duplicate key error", + })) + + resp, err := cl.DeleteOne(context.Background(), mt.Coll.Name(), bson.D{{}}) + + assert.Equal(t, int64(0), resp) + assert.NotNil(t, err) + }) + + mt.Run("DeleteMany", func(mt *mtest.T) { + cl.Database = mt.DB + mt.AddMockResponses(mtest.CreateSuccessResponse()) + + resp, err := cl.DeleteMany(context.Background(), mt.Coll.Name(), bson.D{{}}) + + assert.Equal(t, int64(0), resp) + assert.Nil(t, err) + }) + + mt.Run("DeleteManyError", func(mt *mtest.T) { + cl.Database = mt.DB + mt.AddMockResponses(mtest.CreateWriteErrorsResponse(mtest.WriteError{ + Index: 1, + Code: 11000, + Message: "duplicate key error", + })) + + resp, err := cl.DeleteMany(context.Background(), mt.Coll.Name(), bson.D{{}}) + + assert.Equal(t, int64(0), resp) + assert.NotNil(t, err) + }) +} + +func Test_Drop(t *testing.T) { + // Create a connected client using the mock database + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) + + metrics := NewMockMetrics(gomock.NewController(t)) + + cl := Client{metrics: metrics} + + metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", + gomock.Any(), "database", gomock.Any(), "type", gomock.Any()) + + cl.logger = NewMockLogger(DEBUG) + + mt.Run("Drop", func(mt *mtest.T) { + cl.Database = mt.DB + mt.AddMockResponses(mtest.CreateSuccessResponse()) + + err := cl.Drop(context.Background(), mt.Coll.Name()) + + assert.Nil(t, err) + }) +} + +func Test_HealthCheck(t *testing.T) { + // Create a connected client using the mock database + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) + + metrics := NewMockMetrics(gomock.NewController(t)) + + cl := Client{metrics: metrics} + + cl.logger = NewMockLogger(DEBUG) + + mt.Run("HealthCheck Success", func(mt *mtest.T) { + cl.Database = mt.DB + mt.AddMockResponses(mtest.CreateSuccessResponse()) + + resp := cl.HealthCheck() + + assert.Contains(t, fmt.Sprint(resp), "UP") + }) + + mt.Run("HealthCheck Error", func(mt *mtest.T) { + cl.Database = mt.DB + mt.AddMockResponses(mtest.CreateWriteErrorsResponse(mtest.WriteError{ + Index: 1, + Code: 11000, + Message: "duplicate key error", + })) + + resp := cl.HealthCheck() + + assert.Contains(t, fmt.Sprint(resp), "DOWN") + }) +} diff --git a/pkg/gofr/datasource/pubsub/google/google.go b/pkg/gofr/datasource/pubsub/google/google.go index cb185db02..9e62e82ff 100644 --- a/pkg/gofr/datasource/pubsub/google/google.go +++ b/pkg/gofr/datasource/pubsub/google/google.go @@ -36,11 +36,13 @@ type googleClient struct { func New(conf Config, logger pubsub.Logger, metrics Metrics) *googleClient { err := validateConfigs(&conf) if err != nil { - logger.Errorf("google pubsub could not be configured, err: %v", err) + logger.Errorf("could not configure google pubsub, error: %v", err) return nil } + logger.Debugf("connecting to google pubsub client with projectID '%s' and subscriptionName '%s", conf.ProjectID, conf.SubscriptionName) + client, err := gcPubSub.NewClient(context.Background(), conf.ProjectID) if err != nil { return &googleClient{ @@ -78,7 +80,7 @@ func (g *googleClient) Publish(ctx context.Context, topic string, message []byte t, err := g.getTopic(ctx, topic) if err != nil { - g.logger.Errorf("error creating %s err: %v", topic, err) + g.logger.Errorf("could not create topic '%s', error: %v", topic, err) return err } @@ -92,7 +94,7 @@ func (g *googleClient) Publish(ctx context.Context, topic string, message []byte _, err = result.Get(ctx) if err != nil { - g.logger.Errorf("error publishing to google topic %s err: %v", topic, err) + g.logger.Errorf("error publishing to google topic '%s', error: %v", topic, err) return err } @@ -116,7 +118,7 @@ func (g *googleClient) Subscribe(ctx context.Context, topic string) (*pubsub.Mes ctx, span := otel.GetTracerProvider().Tracer("gofr").Start(ctx, "gcp-subscribe") defer span.End() - g.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_total_count", "topic", topic) + g.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_total_count", "topic", topic, "subscription_name", g.Config.SubscriptionName) var m = pubsub.NewMessage(ctx) @@ -160,7 +162,7 @@ func (g *googleClient) Subscribe(ctx context.Context, topic string) (*pubsub.Mes return nil, err } - g.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_success_count", "topic", topic) + g.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_success_count", "topic", topic, "subscription_name", g.Config.SubscriptionName) return m, nil } @@ -185,7 +187,7 @@ func (g *googleClient) getSubscription(ctx context.Context, topic *gcPubSub.Topi // check if subscription already exists or not ok, err := subscription.Exists(context.Background()) if err != nil { - g.logger.Errorf("unable to check the existence of subscription, err: %v ", err.Error()) + g.logger.Errorf("unable to check the existence of subscription, error: %v", err.Error()) return nil, err } diff --git a/pkg/gofr/datasource/pubsub/google/google_test.go b/pkg/gofr/datasource/pubsub/google/google_test.go index 1bdce039b..5b8a1f09d 100644 --- a/pkg/gofr/datasource/pubsub/google/google_test.go +++ b/pkg/gofr/datasource/pubsub/google/google_test.go @@ -47,7 +47,7 @@ func TestGoogleClient_New_Error(t *testing.T) { }) assert.Nil(t, g) - assert.Contains(t, out, "google pubsub could not be configured") + assert.Contains(t, out, "could not configure google pubsub") } func TestGoogleClient_Publish_Success(t *testing.T) { diff --git a/pkg/gofr/datasource/pubsub/kafka/kafka.go b/pkg/gofr/datasource/pubsub/kafka/kafka.go index 7cd500e3e..d341ced54 100644 --- a/pkg/gofr/datasource/pubsub/kafka/kafka.go +++ b/pkg/gofr/datasource/pubsub/kafka/kafka.go @@ -18,6 +18,15 @@ var ( ErrConsumerGroupNotProvided = errors.New("consumer group id not provided") errBrokerNotProvided = errors.New("kafka broker address not provided") errPublisherNotConfigured = errors.New("can't publish message. Publisher not configured or topic is empty") + errBatchSize = errors.New("KAFKA_BATCH_SIZE must be greater than 0") + errBatchBytes = errors.New("KAFKA_BATCH_BYTES must be greater than 0") + errBatchTimeout = errors.New("KAFKA_BATCH_TIMEOUT must be greater than 0") +) + +const ( + DefaultBatchSize = 100 + DefaultBatchBytes = 1048576 + DefaultBatchTimeout = 1000 ) type Config struct { @@ -25,6 +34,9 @@ type Config struct { Partition int ConsumerGroupID string OffSet int + BatchSize int + BatchBytes int + BatchTimeout int } type kafkaClient struct { @@ -45,14 +57,16 @@ type kafkaClient struct { func New(conf Config, logger pubsub.Logger, metrics Metrics) *kafkaClient { err := validateConfigs(conf) if err != nil { - logger.Errorf("could not initialize kafka, err: %v", err) + logger.Errorf("could not initialize kafka, error: %v", err) return nil } + logger.Debugf("connecting to kafka broker '%s'", conf.Broker) + conn, err := kafka.Dial("tcp", conf.Broker) if err != nil { - logger.Errorf("failed to connect to KAFKA at %v", conf.Broker) + logger.Errorf("failed to connect to kafka at %v, error: %v", conf.Broker, err) return &kafkaClient{ logger: logger, @@ -67,13 +81,16 @@ func New(conf Config, logger pubsub.Logger, metrics Metrics) *kafkaClient { } writer := kafka.NewWriter(kafka.WriterConfig{ - Brokers: []string{conf.Broker}, - Dialer: dialer, + Brokers: []string{conf.Broker}, + Dialer: dialer, + BatchSize: conf.BatchSize, + BatchBytes: conf.BatchBytes, + BatchTimeout: time.Duration(conf.BatchTimeout), }) 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, @@ -92,6 +109,18 @@ func validateConfigs(conf Config) error { return errBrokerNotProvided } + if conf.BatchSize <= 0 { + return errBatchSize + } + + if conf.BatchBytes <= 0 { + return errBatchBytes + } + + if conf.BatchTimeout <= 0 { + return errBatchTimeout + } + return nil } @@ -116,7 +145,7 @@ func (k *kafkaClient) Publish(ctx context.Context, topic string, message []byte) end := time.Since(start) if err != nil { - k.logger.Error("failed to publish message to kafka broker") + k.logger.Errorf("failed to publish message to kafka broker, error: %v", err) return err } @@ -143,7 +172,7 @@ func (k *kafkaClient) Subscribe(ctx context.Context, topic string) (*pubsub.Mess ctx, span := otel.GetTracerProvider().Tracer("gofr").Start(ctx, "kafka-subscribe") defer span.End() - k.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_total_count", "topic", topic) + k.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_total_count", "topic", topic, "consumer_group", k.config.ConsumerGroupID) var reader Reader // Lock the reader map to ensure only one subscriber access the reader at a time @@ -163,7 +192,7 @@ func (k *kafkaClient) Subscribe(ctx context.Context, topic string) (*pubsub.Mess msg, err := reader.ReadMessage(ctx) if err != nil { - k.logger.Errorf("failed to read message from Kafka topic %s: %v", topic, err) + k.logger.Errorf("failed to read message from kafka topic %s: %v", topic, err) return nil, err } @@ -185,7 +214,7 @@ func (k *kafkaClient) Subscribe(ctx context.Context, topic string) (*pubsub.Mess Time: end.Microseconds(), }) - k.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_success_count", "topic", topic) + k.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_success_count", "topic", topic, "consumer_group", k.config.ConsumerGroupID) return m, err } @@ -193,7 +222,7 @@ func (k *kafkaClient) Subscribe(ctx context.Context, topic string) (*pubsub.Mess func (k *kafkaClient) Close() error { err := k.writer.Close() if err != nil { - k.logger.Errorf("failed to close Kafka writer: %v", err) + k.logger.Errorf("failed to close kafka writer, error: %v", err) return err } diff --git a/pkg/gofr/datasource/pubsub/kafka/kafka_test.go b/pkg/gofr/datasource/pubsub/kafka/kafka_test.go index b7891f7b1..0c608107c 100644 --- a/pkg/gofr/datasource/pubsub/kafka/kafka_test.go +++ b/pkg/gofr/datasource/pubsub/kafka/kafka_test.go @@ -2,6 +2,7 @@ package kafka import ( "context" + "errors" "sync" "testing" @@ -14,20 +15,47 @@ import ( "gofr.dev/pkg/gofr/testutil" ) -func Test_validateConfigs(t *testing.T) { - config := Config{Broker: "kafkabroker", ConsumerGroupID: "1"} - - err := validateConfigs(config) - - assert.Nil(t, err) -} - -func Test_validateConfigsErrBrokerNotProvided(t *testing.T) { - config := Config{ConsumerGroupID: "1"} - - err := validateConfigs(config) +func TestValidateConfigs(t *testing.T) { + testCases := []struct { + name string + config Config + expected error + }{ + { + name: "Valid Config", + config: Config{Broker: "kafkabroker", BatchSize: 1, BatchBytes: 1, BatchTimeout: 1}, + expected: nil, + }, + { + name: "Empty Broker", + config: Config{BatchSize: 1, BatchBytes: 1, BatchTimeout: 1}, + expected: errBrokerNotProvided, + }, + { + name: "Zero BatchSize", + config: Config{Broker: "kafkabroker", BatchSize: 0, BatchBytes: 1, BatchTimeout: 1}, + expected: errBatchSize, + }, + { + name: "Zero BatchBytes", + config: Config{Broker: "kafkabroker", BatchSize: 1, BatchBytes: 0, BatchTimeout: 1}, + expected: errBatchBytes, + }, + { + name: "Zero BatchTimeout", + config: Config{Broker: "kafkabroker", BatchSize: 1, BatchBytes: 1, BatchTimeout: 0}, + expected: errBatchTimeout, + }, + } - assert.Equal(t, err, errBrokerNotProvided) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := validateConfigs(tc.config) + if !errors.Is(err, tc.expected) { + t.Errorf("Expected error %v, but got %v", tc.expected, err) + } + }) + } } func TestKafkaClient_PublishError(t *testing.T) { @@ -155,8 +183,10 @@ func TestKafkaClient_SubscribeSuccess(t *testing.T) { mockReader.EXPECT().ReadMessage(gomock.Any()). Return(kafka.Message{Value: []byte(`hello`), Topic: "test"}, nil) - mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_total_count", "topic", "test") - mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_success_count", "topic", "test") + mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_total_count", "topic", "test", + "consumer_group", gomock.Any()) + mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_success_count", "topic", "test", + "consumer_group", gomock.Any()) logs := testutil.StdoutOutputForFunc(func() { logger := logging.NewMockLogger(logging.DEBUG) @@ -220,7 +250,8 @@ func TestKafkaClient_SubscribeError(t *testing.T) { mockReader.EXPECT().ReadMessage(gomock.Any()). Return(kafka.Message{}, errSub) - mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_total_count", "topic", "test") + mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_total_count", + "topic", "test", "consumer_group", k.config.ConsumerGroupID) logs := testutil.StderrOutputForFunc(func() { logger := logging.NewMockLogger(logging.DEBUG) @@ -232,7 +263,7 @@ func TestKafkaClient_SubscribeError(t *testing.T) { assert.NotNil(t, err) assert.Equal(t, errSub, err) assert.Nil(t, msg) - assert.Contains(t, logs, "failed to read message from Kafka topic test: error while subscribing") + assert.Contains(t, logs, "failed to read message from kafka topic test: error while subscribing") } func TestKafkaClient_Close(t *testing.T) { @@ -272,7 +303,7 @@ func TestKafkaClient_CloseError(t *testing.T) { assert.NotNil(t, err) assert.Equal(t, errClose, err) - assert.Contains(t, logs, "failed to close Kafka writer") + assert.Contains(t, logs, "failed to close kafka writer") } func TestKafkaClient_getNewReader(t *testing.T) { @@ -295,28 +326,66 @@ func TestNewKafkaClient(t *testing.T) { defer ctrl.Finish() testCases := []struct { - desc string - config Config + desc string + config Config + expectNil bool }{ { - desc: "validation of configs fail", + desc: "validation of configs fail (Empty Broker)", config: Config{ - Broker: "kafka-broker", + Broker: "", }, + expectNil: true, + }, + { + desc: "validation of configs fail (Zero Batch Bytes)", + config: Config{ + Broker: "kafka-broker", + BatchBytes: 0, + }, + expectNil: true, + }, + { + desc: "validation of configs fail (Zero Batch Size)", + config: Config{ + Broker: "kafka-broker", + BatchBytes: 1, + BatchSize: 0, + }, + expectNil: true, + }, + { + desc: "validation of configs fail (Zero Batch Timeout)", + config: Config{ + Broker: "kafka-broker", + BatchBytes: 1, + BatchSize: 1, + BatchTimeout: 0, + }, + expectNil: true, }, { desc: "successful initialization", config: Config{ Broker: "kafka-broker", ConsumerGroupID: "consumer", + BatchBytes: 1, + BatchSize: 1, + BatchTimeout: 1, }, + expectNil: false, }, } for _, tc := range testCases { - k := New(tc.config, logging.NewMockLogger(logging.ERROR), NewMockMetrics(ctrl)) - - assert.NotNil(t, k) + t.Run(tc.desc, func(t *testing.T) { + k := New(tc.config, logging.NewMockLogger(logging.ERROR), NewMockMetrics(ctrl)) + if tc.expectNil { + assert.Nil(t, k) + } else { + assert.NotNil(t, k) + } + }) } } diff --git a/pkg/gofr/datasource/pubsub/mqtt/mqtt.go b/pkg/gofr/datasource/pubsub/mqtt/mqtt.go index a45a7f530..cd6b50730 100644 --- a/pkg/gofr/datasource/pubsub/mqtt/mqtt.go +++ b/pkg/gofr/datasource/pubsub/mqtt/mqtt.go @@ -61,18 +61,20 @@ func New(config *Config, logger Logger, metrics Metrics) *MQTT { options := getMQTTClientOptions(config) + logger.Debugf("connecting to MQTT at '%v:%v' with clientID '%v'", config.Hostname, config.Port, config.ClientID) + // create the client using the options above client := mqtt.NewClient(options) if token := client.Connect(); token.Wait() && token.Error() != nil { - logger.Errorf("cannot connect to MQTT, host: %v, port: %v, error: %v", config.Hostname, config.Port, token.Error()) + logger.Errorf("could not connect to MQTT at '%v:%v', error: %v", config.Hostname, config.Port, token.Error()) return &MQTT{Client: client, config: config, logger: logger} } msg := make(map[string]chan *pubsub.Message) - logger.Infof("connected to MQTT at %v:%v, clientID: %v", config.Hostname, config.Port, options.ClientID) + logger.Infof("connected to MQTT at '%v:%v' with clientID '%v'", config.Hostname, config.Port, options.ClientID) return &MQTT{Client: client, config: config, logger: logger, msgChanMap: msg, mu: new(sync.RWMutex), metrics: metrics} } @@ -90,7 +92,7 @@ func getDefaultClient(config *Config, logger Logger, metrics Metrics) *MQTT { client := mqtt.NewClient(opts) if token := client.Connect(); token.Wait() && token.Error() != nil { - logger.Errorf("cannot connect to MQTT, host: %v, port: %v, error: %v", host, port, token.Error()) + logger.Errorf("could not connect to MQTT at '%v:%v', error: %v", config.Hostname, config.Port, token.Error()) return &MQTT{Client: client, config: config, logger: logger} } @@ -101,7 +103,7 @@ func getDefaultClient(config *Config, logger Logger, metrics Metrics) *MQTT { msg := make(map[string]chan *pubsub.Message) - logger.Infof("connected to MQTT at %v:%v, clientID: %v", config.Hostname, config.Port, clientID) + logger.Infof("connected to MQTT at '%v:%v' with clientID '%v'", config.Hostname, config.Port, clientID) return &MQTT{Client: client, config: config, logger: logger, msgChanMap: msg, mu: new(sync.RWMutex), metrics: metrics} } @@ -184,7 +186,7 @@ func (m *MQTT) Subscribe(ctx context.Context, topic string) (*pubsub.Message, er token := m.Client.Subscribe(topic, m.config.QoS, handler) if token.Wait() && token.Error() != nil { - m.logger.Errorf("error getting a message from MQTT, err: %v", token.Error()) + m.logger.Errorf("error getting a message from MQTT, error: %v", token.Error()) return nil, token.Error() } @@ -208,7 +210,7 @@ func (m *MQTT) Publish(ctx context.Context, topic string, message []byte) error // Check for errors during publishing (More on error reporting // https://pkg.go.dev/github.com/eclipse/paho.mqtt.golang#readme-error-handling) if token.Wait() && token.Error() != nil { - m.logger.Errorf("error while publishing message, err %v", token.Error()) + m.logger.Errorf("error while publishing message, error: %v", token.Error()) return token.Error() } @@ -262,7 +264,7 @@ func (m *MQTT) CreateTopic(_ context.Context, topic string) error { token.Wait() if token.Error() != nil { - m.logger.Errorf("unable to create topic - %s, error: %v", topic, token.Error()) + m.logger.Errorf("unable to create topic '%s', error: %v", topic, token.Error()) return token.Error() } @@ -312,7 +314,7 @@ func (m *MQTT) Unsubscribe(topic string) error { token.Wait() if token.Error() != nil { - m.logger.Errorf("error while unsubscribing from topic %s, err: %v", topic, token.Error()) + m.logger.Errorf("error while unsubscribing from topic '%s', error: %v", topic, token.Error()) return token.Error() } diff --git a/pkg/gofr/datasource/pubsub/mqtt/mqtt_test.go b/pkg/gofr/datasource/pubsub/mqtt/mqtt_test.go index 3f9eb976b..b60ab1cd4 100644 --- a/pkg/gofr/datasource/pubsub/mqtt/mqtt_test.go +++ b/pkg/gofr/datasource/pubsub/mqtt/mqtt_test.go @@ -36,7 +36,7 @@ func TestMQTT_New(t *testing.T) { }) assert.NotNil(t, client.Client) - assert.Contains(t, out, "cannot connect to MQTT") + assert.Contains(t, out, "could not connect to MQTT") } // TestMQTT_EmptyConfigs test the scenario where configs are not provided and @@ -227,25 +227,25 @@ func TestMQTT_SubscribeFailure(t *testing.T) { } func TestMQTT_SubscribeWithFunc(t *testing.T) { - subcriptionFunc := func(msg *pubsub.Message) error { + subscriptionFunc := func(msg *pubsub.Message) error { assert.NotNil(t, msg) assert.Equal(t, "test/topic", msg.Topic) return nil } - subcriptionFuncErr := func(*pubsub.Message) error { + subscriptionFuncErr := func(*pubsub.Message) error { return errTest } m := New(&Config{}, logging.NewMockLogger(logging.ERROR), nil) // Success case - err := m.SubscribeWithFunction("test/topic", subcriptionFunc) + err := m.SubscribeWithFunction("test/topic", subscriptionFunc) assert.Nil(t, err) // Error case where error is returned from subscription function - err = m.SubscribeWithFunction("test/topic", subcriptionFuncErr) + err = m.SubscribeWithFunction("test/topic", subscriptionFuncErr) assert.Nil(t, err) // Unsubscribe from the topic @@ -253,7 +253,7 @@ func TestMQTT_SubscribeWithFunc(t *testing.T) { // Error case where the client cannot connect m.Disconnect(1) - err = m.SubscribeWithFunction("test/topic", subcriptionFunc) + err = m.SubscribeWithFunction("test/topic", subscriptionFunc) assert.NotNil(t, err) } @@ -272,7 +272,7 @@ func TestMQTT_Unsubscribe(t *testing.T) { assert.NotNil(t, err) }) - assert.Contains(t, out, "error while unsubscribing from topic test/topic") + assert.Contains(t, out, "error while unsubscribing from topic 'test/topic'") } func TestMQTT_CreateTopic(t *testing.T) { @@ -290,7 +290,7 @@ func TestMQTT_CreateTopic(t *testing.T) { assert.NotNil(t, err) }) - assert.Contains(t, out, "unable to create topic - test/topic") + assert.Contains(t, out, "unable to create topic 'test/topic'") } func TestMQTT_Health(t *testing.T) { diff --git a/pkg/gofr/datasource/redis/health_test.go b/pkg/gofr/datasource/redis/health_test.go index 746d17341..2d37cd9b8 100644 --- a/pkg/gofr/datasource/redis/health_test.go +++ b/pkg/gofr/datasource/redis/health_test.go @@ -23,8 +23,10 @@ func TestRedis_HealthHandlerError(t *testing.T) { defer s.Close() mockMetric := NewMockMetrics(ctrl) - mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), "type", "ping") - mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), "type", "info") + mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), + "hostname", gomock.Any(), "type", "ping") + mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), + "hostname", gomock.Any(), "type", "info") client := NewClient(config.NewMockConfig(map[string]string{ "REDIS_HOST": s.Host(), diff --git a/pkg/gofr/datasource/redis/hook.go b/pkg/gofr/datasource/redis/hook.go index 0a84bbd19..78b68cc54 100644 --- a/pkg/gofr/datasource/redis/hook.go +++ b/pkg/gofr/datasource/redis/hook.go @@ -15,6 +15,7 @@ import ( // redisHook is a custom Redis hook for logging queries and their durations. type redisHook struct { + config *Config logger datasource.Logger metrics Metrics } @@ -73,7 +74,7 @@ func (r *redisHook) logQuery(start time.Time, query string, args ...interface{}) }) r.metrics.RecordHistogram(context.Background(), "app_redis_stats", - float64(duration), "type", query) + float64(duration), "hostname", r.config.HostName, "type", query) } // DialHook implements the redis.DialHook interface. diff --git a/pkg/gofr/datasource/redis/redis.go b/pkg/gofr/datasource/redis/redis.go index 5d993000a..a6272514f 100644 --- a/pkg/gofr/datasource/redis/redis.go +++ b/pkg/gofr/datasource/redis/redis.go @@ -33,35 +33,23 @@ type Redis struct { // NewClient return a redis client if connection is successful based on Config. // In case of error, it returns an error as second parameter. func NewClient(c config.Config, logger datasource.Logger, metrics Metrics) *Redis { - var redisConfig = &Config{} + redisConfig := getRedisConfig(c) - if redisConfig.HostName = c.Get("REDIS_HOST"); redisConfig.HostName == "" { + // if Hostname is not provided, we won't try to connect to Redis + if redisConfig.HostName == "" { return nil } - port, err := strconv.Atoi(c.Get("REDIS_PORT")) - if err != nil { - port = defaultRedisPort - } - - redisConfig.Port = port - - options := new(redis.Options) - - if options.Addr == "" { - options.Addr = fmt.Sprintf("%s:%d", redisConfig.HostName, redisConfig.Port) - } - - redisConfig.Options = options + logger.Debugf("connecting to redis at '%s:%d'", redisConfig.HostName, redisConfig.Port) rc := redis.NewClient(redisConfig.Options) - rc.AddHook(&redisHook{logger: logger, metrics: metrics}) + rc.AddHook(&redisHook{config: redisConfig, logger: logger, metrics: metrics}) ctx, cancel := context.WithTimeout(context.TODO(), redisPingTimeout) defer cancel() if err := rc.Ping(ctx).Err(); err != nil { - logger.Errorf("could not connect to redis at %s:%d. error: %s", redisConfig.HostName, redisConfig.Port, err) + logger.Errorf("could not connect to redis at '%s:%d', error: %s", redisConfig.HostName, redisConfig.Port, err) return &Redis{Client: nil, config: redisConfig, logger: logger} } @@ -75,6 +63,29 @@ func NewClient(c config.Config, logger datasource.Logger, metrics Metrics) *Redi return &Redis{Client: rc, config: redisConfig, logger: logger} } +func getRedisConfig(c config.Config) *Config { + var redisConfig = &Config{} + + redisConfig.HostName = c.Get("REDIS_HOST") + + port, err := strconv.Atoi(c.Get("REDIS_PORT")) + if err != nil { + port = defaultRedisPort + } + + redisConfig.Port = port + + options := new(redis.Options) + + if options.Addr == "" { + options.Addr = fmt.Sprintf("%s:%d", redisConfig.HostName, redisConfig.Port) + } + + redisConfig.Options = options + + return redisConfig +} + // TODO - if we make Redis an interface and expose from container we can avoid c.Redis(c, command) using methods on c and still pass c. // type Redis interface { // Get(string) (string, error) diff --git a/pkg/gofr/datasource/redis/redis_test.go b/pkg/gofr/datasource/redis/redis_test.go index 80ed1f971..40d22c937 100644 --- a/pkg/gofr/datasource/redis/redis_test.go +++ b/pkg/gofr/datasource/redis/redis_test.go @@ -33,10 +33,9 @@ func Test_NewClient_InvalidPort(t *testing.T) { mockLogger := logging.NewMockLogger(logging.ERROR) mockMetrics := NewMockMetrics(ctrl) - mockConfig := config.NewMockConfig(map[string]string{"REDIS_HOST": "localhost", - "REDIS_PORT": "&&^%%^&*"}) + mockConfig := config.NewMockConfig(map[string]string{"REDIS_HOST": "localhost", "REDIS_PORT": "&&^%%^&*"}) - mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), "type", "ping") + mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), "hostname", gomock.Any(), "type", "ping") client := NewClient(mockConfig, mockLogger, mockMetrics) assert.Nil(t, client.Client, "Test_NewClient_InvalidPort Failed! Expected redis client to be nil") @@ -53,8 +52,8 @@ func TestRedis_QueryLogging(t *testing.T) { defer s.Close() mockMetric := NewMockMetrics(ctrl) - mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), "type", "ping") - mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), "type", "set") + mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), "hostname", gomock.Any(), "type", "ping") + mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), "hostname", gomock.Any(), "type", "set") result := testutil.StdoutOutputForFunc(func() { mockLogger := logging.NewMockLogger(logging.DEBUG) @@ -87,8 +86,8 @@ func TestRedis_PipelineQueryLogging(t *testing.T) { defer s.Close() mockMetric := NewMockMetrics(ctrl) - mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), "type", "ping") - mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), "type", "pipeline") + mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), "hostname", gomock.Any(), "type", "ping") + mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), "hostname", gomock.Any(), "type", "pipeline") // Execute Redis pipeline result := testutil.StdoutOutputForFunc(func() { diff --git a/pkg/gofr/datasource/sql/db.go b/pkg/gofr/datasource/sql/db.go index 5ffa293dc..e0b09ace7 100644 --- a/pkg/gofr/datasource/sql/db.go +++ b/pkg/gofr/datasource/sql/db.go @@ -54,8 +54,8 @@ func (d *DB) logQuery(start time.Time, queryType, query string, args ...interfac Args: args, }) - d.metrics.RecordHistogram(context.Background(), "app_sql_stats", float64(duration), - "type", getOperationType(query)) + d.metrics.RecordHistogram(context.Background(), "app_sql_stats", float64(duration), "hostname", d.config.HostName, + "database", d.config.Database, "type", getOperationType(query)) } func getOperationType(query string) string { @@ -105,11 +105,12 @@ func (d *DB) Begin() (*Tx, error) { return nil, err } - return &Tx{Tx: tx, logger: d.logger, metrics: d.metrics}, nil + return &Tx{Tx: tx, config: d.config, logger: d.logger, metrics: d.metrics}, nil } type Tx struct { *sql.Tx + config *DBConfig logger datasource.Logger metrics Metrics } @@ -124,8 +125,8 @@ func (t *Tx) logQuery(start time.Time, queryType, query string, args ...interfac Args: args, }) - t.metrics.RecordHistogram(context.Background(), "app_sql_stats", float64(duration), - "type", getOperationType(query)) + t.metrics.RecordHistogram(context.Background(), "app_sql_stats", float64(duration), "hostname", t.config.HostName, + "database", t.config.Database, "type", getOperationType(query)) } func (t *Tx) Query(query string, args ...interface{}) (*sql.Rows, error) { diff --git a/pkg/gofr/datasource/sql/db_test.go b/pkg/gofr/datasource/sql/db_test.go index 80bc7a602..e6dd6b3e2 100644 --- a/pkg/gofr/datasource/sql/db_test.go +++ b/pkg/gofr/datasource/sql/db_test.go @@ -27,7 +27,10 @@ func getDB(t *testing.T, logLevel logging.Level) (*DB, sqlmock.Sqlmock) { t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) } - return &DB{mockDB, logging.NewMockLogger(logLevel), nil, nil}, mock + db := &DB{mockDB, logging.NewMockLogger(logLevel), nil, nil} + db.config = &DBConfig{} + + return db, mock } func TestDB_SelectSingleColumnFromIntToString(t *testing.T) { @@ -293,7 +296,7 @@ func TestDB_Query(t *testing.T) { mock.ExpectQuery("SELECT 1"). WillReturnRows(sqlmock.NewRows([]string{"1"}).AddRow("1")) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "SELECT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT") rows, err = db.Query("SELECT 1") assert.Nil(t, err) @@ -322,7 +325,7 @@ func TestDB_QueryError(t *testing.T) { mock.ExpectQuery("SELECT "). WillReturnError(errSyntax) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "SELECT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT") rows, err = db.Query("SELECT") if !assert.Nil(t, rows) { @@ -353,7 +356,7 @@ func TestDB_QueryRow(t *testing.T) { mock.ExpectQuery("SELECT name FROM employee WHERE id = ?").WithArgs(1). WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow("jhon")) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "SELECT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT") row = db.QueryRow("SELECT name FROM employee WHERE id = ?", 1) assert.NotNil(t, row) @@ -378,7 +381,7 @@ func TestDB_QueryRowContext(t *testing.T) { mock.ExpectQuery("SELECT name FROM employee WHERE id = ?").WithArgs(1) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "SELECT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT") row = db.QueryRowContext(context.Background(), "SELECT name FROM employee WHERE id = ?", 1) assert.NotNil(t, row) @@ -405,7 +408,7 @@ func TestDB_Exec(t *testing.T) { mock.ExpectExec("INSERT INTO employee VALUES(?, ?)"). WithArgs(2, "doe").WillReturnResult(sqlmock.NewResult(1, 1)) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "INSERT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT") res, err = db.Exec("INSERT INTO employee VALUES(?, ?)", 2, "doe") assert.Nil(t, err) @@ -433,7 +436,7 @@ func TestDB_ExecError(t *testing.T) { mock.ExpectExec("INSERT INTO employee VALUES(?, ?"). WithArgs(2, "doe").WillReturnError(errSyntax) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "INSERT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT") res, err = db.Exec("INSERT INTO employee VALUES(?, ?", 2, "doe") assert.Nil(t, res) @@ -462,7 +465,7 @@ func TestDB_ExecContext(t *testing.T) { mock.ExpectExec(`INSERT INTO employee VALUES(?, ?)`). WithArgs(2, "doe").WillReturnResult(sqlmock.NewResult(1, 1)) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "INSERT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT") res, err = db.ExecContext(context.Background(), "INSERT INTO employee VALUES(?, ?)", 2, "doe") assert.Nil(t, err) @@ -490,7 +493,7 @@ func TestDB_ExecContextError(t *testing.T) { mock.ExpectExec(`INSERT INTO employee VALUES(?, ?)`). WithArgs(2, "doe").WillReturnResult(sqlmock.NewResult(1, 1)) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "INSERT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT") res, err = db.ExecContext(context.Background(), "INSERT INTO employee VALUES(?, ?)", 2, "doe") assert.Nil(t, err) @@ -517,7 +520,7 @@ func TestDB_Prepare(t *testing.T) { mock.ExpectPrepare("SELECT name FROM employee WHERE id = ?") mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "SELECT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT") stmt, err = db.Prepare("SELECT name FROM employee WHERE id = ?") assert.Nil(t, err) @@ -544,7 +547,7 @@ func TestDB_PrepareError(t *testing.T) { mock.ExpectPrepare("SELECT name FROM employee WHERE id = ?") mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "SELECT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT") stmt, err = db.Prepare("SELECT name FROM employee WHERE id = ?") assert.Nil(t, err) @@ -605,7 +608,7 @@ func TestTx_Query(t *testing.T) { mock.ExpectQuery("SELECT 1"). WillReturnRows(sqlmock.NewRows([]string{"1"}).AddRow("1")) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "SELECT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT") rows, err = tx.Query("SELECT 1") assert.Nil(t, err) @@ -635,7 +638,7 @@ func TestTx_QueryError(t *testing.T) { mock.ExpectQuery("SELECT "). WillReturnError(errSyntax) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "SELECT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT") rows, err = tx.Query("SELECT") if !assert.Nil(t, rows) { @@ -668,7 +671,7 @@ func TestTx_QueryRow(t *testing.T) { mock.ExpectQuery("SELECT name FROM employee WHERE id = ?").WithArgs(1). WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow("jhon")) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "SELECT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT") row = tx.QueryRow("SELECT name FROM employee WHERE id = ?", 1) assert.NotNil(t, row) @@ -695,7 +698,7 @@ func TestTx_QueryRowContext(t *testing.T) { mock.ExpectQuery("SELECT name FROM employee WHERE id = ?").WithArgs(1) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "SELECT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT") row = tx.QueryRowContext(context.Background(), "SELECT name FROM employee WHERE id = ?", 1) assert.NotNil(t, row) @@ -724,7 +727,7 @@ func TestTx_Exec(t *testing.T) { mock.ExpectExec("INSERT INTO employee VALUES(?, ?)"). WithArgs(2, "doe").WillReturnResult(sqlmock.NewResult(1, 1)) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "INSERT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT") res, err = tx.Exec("INSERT INTO employee VALUES(?, ?)", 2, "doe") assert.Nil(t, err) @@ -754,7 +757,7 @@ func TestTx_ExecError(t *testing.T) { mock.ExpectExec("INSERT INTO employee VALUES(?, ?"). WithArgs(2, "doe").WillReturnError(errSyntax) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "INSERT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT") res, err = tx.Exec("INSERT INTO employee VALUES(?, ?", 2, "doe") assert.Nil(t, res) @@ -785,7 +788,7 @@ func TestTx_ExecContext(t *testing.T) { mock.ExpectExec(`INSERT INTO employee VALUES(?, ?)`). WithArgs(2, "doe").WillReturnResult(sqlmock.NewResult(1, 1)) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "INSERT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT") res, err = tx.ExecContext(context.Background(), "INSERT INTO employee VALUES(?, ?)", 2, "doe") assert.Nil(t, err) @@ -815,7 +818,7 @@ func TestTx_ExecContextError(t *testing.T) { mock.ExpectExec(`INSERT INTO employee VALUES(?, ?)`). WithArgs(2, "doe").WillReturnResult(sqlmock.NewResult(1, 1)) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "INSERT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT") res, err = tx.ExecContext(context.Background(), "INSERT INTO employee VALUES(?, ?)", 2, "doe") assert.Nil(t, err) @@ -844,7 +847,7 @@ func TestTx_Prepare(t *testing.T) { mock.ExpectPrepare("SELECT name FROM employee WHERE id = ?") mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "SELECT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT") stmt, err = tx.Prepare("SELECT name FROM employee WHERE id = ?") assert.Nil(t, err) @@ -873,7 +876,7 @@ func TestTx_PrepareError(t *testing.T) { mock.ExpectPrepare("SELECT name FROM employee WHERE id = ?") mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "SELECT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT") stmt, err = tx.Prepare("SELECT name FROM employee WHERE id = ?") assert.Nil(t, err) @@ -897,7 +900,7 @@ func TestTx_Commit(t *testing.T) { tx := getTransaction(db, mock) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "COMMIT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "COMMIT") mock.ExpectCommit() err = tx.Commit() @@ -921,7 +924,7 @@ func TestTx_CommitError(t *testing.T) { tx := getTransaction(db, mock) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "COMMIT") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "COMMIT") mock.ExpectCommit().WillReturnError(errDB) err = tx.Commit() @@ -946,7 +949,7 @@ func TestTx_RollBack(t *testing.T) { tx := getTransaction(db, mock) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "ROLLBACK") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "ROLLBACK") mock.ExpectRollback() err = tx.Rollback() @@ -970,7 +973,7 @@ func TestTx_RollbackError(t *testing.T) { tx := getTransaction(db, mock) mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", - gomock.Any(), "type", "ROLLBACK") + gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "ROLLBACK") mock.ExpectRollback().WillReturnError(errDB) err = tx.Rollback() diff --git a/pkg/gofr/datasource/sql/sql.go b/pkg/gofr/datasource/sql/sql.go index 5bd505337..8e114a7f8 100644 --- a/pkg/gofr/datasource/sql/sql.go +++ b/pkg/gofr/datasource/sql/sql.go @@ -4,18 +4,23 @@ import ( "database/sql" "fmt" "strconv" + "strings" "time" "github.com/XSAM/otelsql" _ "github.com/lib/pq" // used for concrete implementation of the database driver. + _ "modernc.org/sqlite" "gofr.dev/pkg/gofr/config" "gofr.dev/pkg/gofr/datasource" ) -const defaultDBPort = 3306 +const ( + sqlite = "sqlite" + defaultDBPort = 3306 +) -var errUnsupportedDialect = fmt.Errorf("unsupported db dialect; supported dialects are - mysql, postgres") +var errUnsupportedDialect = fmt.Errorf("unsupported db dialect; supported dialects are - mysql, postgres, sqlite") // DBConfig has those members which are necessary variables while connecting to database. type DBConfig struct { @@ -31,10 +36,13 @@ func NewSQL(configs config.Config, logger datasource.Logger, metrics Metrics) *D dbConfig := getDBConfig(configs) // if Hostname is not provided, we won't try to connect to DB - if dbConfig.HostName == "" || dbConfig.Dialect == "" { + if dbConfig.Dialect != sqlite && dbConfig.HostName == "" { return nil } + logger.Debugf("connecting with '%s' user to '%s' database at '%s:%s'", + dbConfig.User, dbConfig.Database, dbConfig.HostName, dbConfig.Port) + dbConnectionString, err := getDBConnectionString(dbConfig) if err != nil { logger.Error(errUnsupportedDialect) @@ -43,7 +51,7 @@ func NewSQL(configs config.Config, logger datasource.Logger, metrics Metrics) *D otelRegisteredDialect, err := otelsql.Register(dbConfig.Dialect) if err != nil { - logger.Errorf("could not register sql dialect '%s' for traces due to error: '%s'", dbConfig.Dialect, err) + logger.Errorf("could not register sql dialect '%s' for traces, error: %s", dbConfig.Dialect, err) return nil } @@ -51,8 +59,8 @@ func NewSQL(configs config.Config, logger datasource.Logger, metrics Metrics) *D database.DB, err = sql.Open(otelRegisteredDialect, dbConnectionString) if err != nil { - database.logger.Errorf("could not open connection with '%s' user to database '%s:%s' error: %v", - database.config.User, database.config.HostName, database.config.Port, err) + database.logger.Errorf("could not open connection with '%s' user to '%s' database at '%s:%s', error: %v", + database.config.User, database.config.Database, database.config.HostName, database.config.Port, err) return database } @@ -68,13 +76,13 @@ func NewSQL(configs config.Config, logger datasource.Logger, metrics Metrics) *D func pingToTestConnection(database *DB) *DB { if err := database.DB.Ping(); err != nil { - database.logger.Errorf("could not connect with '%s' user to database '%s:%s' error: %v", - database.config.User, database.config.HostName, database.config.Port, err) + database.logger.Errorf("could not connect with '%s' user to '%s' database at '%s:%s', error: %v", + database.config.User, database.config.Database, database.config.HostName, database.config.Port, err) return database } - database.logger.Logf("connected to '%s' database at %s:%s", database.config.Database, + database.logger.Logf("connected to '%s' database at '%s:%s'", database.config.Database, database.config.HostName, database.config.Port) return database @@ -89,12 +97,12 @@ func retryConnection(database *DB) { for { if err := database.DB.Ping(); err != nil { - database.logger.Debugf("could not connect with '%s' user to database '%s:%s' error: %v", + database.logger.Debugf("could not connect with '%s' user to database '%s:%s', error: %v", database.config.User, database.config.HostName, database.config.Port, err) time.Sleep(connRetryFrequencyInSeconds * time.Second) } else { - database.logger.Logf("connected to '%s' database at %s:%s", database.config.Database, + database.logger.Logf("connected to '%s' database at '%s:%s'", database.config.Database, database.config.HostName, database.config.Port) break @@ -130,6 +138,10 @@ func getDBConnectionString(dbConfig *DBConfig) (string, error) { case "postgres": return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", dbConfig.HostName, dbConfig.Port, dbConfig.User, dbConfig.Password, dbConfig.Database), nil + case sqlite: + s := strings.TrimSuffix(dbConfig.Database, ".db") + + return fmt.Sprintf("file:%s.db", s), nil default: return "", errUnsupportedDialect } diff --git a/pkg/gofr/datasource/sql/sql_test.go b/pkg/gofr/datasource/sql/sql_test.go index 893fc2799..736cc2f46 100644 --- a/pkg/gofr/datasource/sql/sql_test.go +++ b/pkg/gofr/datasource/sql/sql_test.go @@ -17,7 +17,7 @@ import ( func TestNewSQL_ErrorCase(t *testing.T) { ctrl := gomock.NewController(t) - expectedLog := fmt.Sprintf("could not register sql dialect '%s' for traces due to error: '%s'", "mysql", + expectedLog := fmt.Sprintf("could not register sql dialect '%s' for traces, error: %s", "mysql", "sql: unknown driver \"mysql\" (forgotten import?)") mockConfig := config.NewMockConfig(map[string]string{ @@ -131,6 +131,14 @@ func TestSQL_getDBConnectionString(t *testing.T) { }, expOut: "host=host port=3201 user=user password=password dbname=test sslmode=disable", }, + { + desc: "sqlite dialect", + configs: &DBConfig{ + Dialect: "sqlite", + Database: "test.db", + }, + expOut: "file:test.db", + }, { desc: "unsupported dialect", configs: &DBConfig{Dialect: "mssql"}, diff --git a/pkg/gofr/externalDB.go b/pkg/gofr/externalDB.go new file mode 100644 index 000000000..92b6451bc --- /dev/null +++ b/pkg/gofr/externalDB.go @@ -0,0 +1,18 @@ +package gofr + +import "gofr.dev/pkg/gofr/datasource" + +func (a *App) AddMongo(db datasource.MongoProvider) { + db.UseLogger(a.Logger()) + db.UseMetrics(a.Metrics()) + + db.Connect() + + a.container.Mongo = db +} + +// UseMongo sets the Mongo datasource in the app's container. +// Deprecated: Use the NewMongo function AddMongo instead. +func (a *App) UseMongo(db datasource.Mongo) { + a.container.Mongo = db +} diff --git a/pkg/gofr/gofr.go b/pkg/gofr/gofr.go index 5694ab560..58511aff1 100644 --- a/pkg/gofr/gofr.go +++ b/pkg/gofr/gofr.go @@ -22,7 +22,6 @@ import ( "gofr.dev/pkg/gofr/config" "gofr.dev/pkg/gofr/container" - "gofr.dev/pkg/gofr/datasource" gofrHTTP "gofr.dev/pkg/gofr/http" "gofr.dev/pkg/gofr/http/middleware" "gofr.dev/pkg/gofr/logging" @@ -31,7 +30,7 @@ import ( "gofr.dev/pkg/gofr/service" ) -// App is the main application in the gofr framework. +// App is the main application in the GoFr framework. type App struct { // Config can be used by applications to fetch custom configurations from environment or file. Config config.Config // If we directly embed, unnecessary confusion between app.Get and app.GET will happen. @@ -53,7 +52,7 @@ type App struct { subscriptionManager SubscriptionManager } -// RegisterService adds a grpc service to the gofr application. +// RegisterService adds a gRPC service to the GoFr application. func (a *App) RegisterService(desc *grpc.ServiceDesc, impl interface{}) { a.container.Logger.Infof("registering GRPC Server: %s", desc.ServiceName) a.grpcServer.server.RegisterService(desc, impl) @@ -97,7 +96,7 @@ func New() *App { return app } -// NewCMD creates a command line application. +// NewCMD creates a command-line application. func NewCMD() *App { app := &App{} app.readConfig(true) @@ -112,7 +111,7 @@ func NewCMD() *App { return app } -// Run starts the application. If it is a HTTP server, it will start the server. +// Run starts the application. If it is an HTTP server, it will start the server. func (a *App) Run() { if a.cmd != nil { a.cmd.Run(a.container) @@ -121,7 +120,7 @@ func (a *App) Run() { wg := sync.WaitGroup{} // Start Metrics Server - // running metrics server before http and grpc + // running metrics server before HTTP and gRPC wg.Add(1) go func(m *metricServer) { @@ -207,27 +206,27 @@ func (a *App) AddHTTPService(serviceName, serviceAddress string, options ...serv a.container.Services[serviceName] = service.NewHTTPService(serviceAddress, a.container.Logger, a.container.Metrics(), options...) } -// GET adds a Handler for http GET method for a route pattern. +// GET adds a Handler for HTTP GET method for a route pattern. func (a *App) GET(pattern string, handler Handler) { a.add("GET", pattern, handler) } -// PUT adds a Handler for http PUT method for a route pattern. +// PUT adds a Handler for HTTP PUT method for a route pattern. func (a *App) PUT(pattern string, handler Handler) { a.add("PUT", pattern, handler) } -// POST adds a Handler for http POST method for a route pattern. +// POST adds a Handler for HTTP POST method for a route pattern. func (a *App) POST(pattern string, handler Handler) { a.add("POST", pattern, handler) } -// DELETE adds a Handler for http DELETE method for a route pattern. +// DELETE adds a Handler for HTTP DELETE method for a route pattern. func (a *App) DELETE(pattern string, handler Handler) { a.add("DELETE", pattern, handler) } -// PATCH adds a Handler for http PATCH method for a route pattern. +// PATCH adds a Handler for HTTP PATCH method for a route pattern. func (a *App) PATCH(pattern string, handler Handler) { a.add("PATCH", pattern, handler) } @@ -235,8 +234,9 @@ func (a *App) PATCH(pattern string, handler Handler) { func (a *App) add(method, pattern string, h Handler) { a.httpRegistered = true a.httpServer.router.Add(method, pattern, handler{ - function: h, - container: a.container, + function: h, + container: a.container, + requestTimeout: a.Config.GetOrDefault("REQUEST_TIMEOUT", "5"), }) } @@ -299,7 +299,7 @@ func (a *App) initTracer() { case traceExporterGoFr: exporter = NewExporter("https://tracer-api.gofr.dev/api/spans", logging.NewLogger(logging.INFO)) - a.container.Log("Exporting traces to gofr at https://tracer.gofr.dev") + a.container.Log("Exporting traces to GoFr at https://tracer.gofr.dev") default: a.container.Error("unsupported trace exporter.") } @@ -387,10 +387,6 @@ func (a *App) UseMiddleware(middlewares ...gofrHTTP.Middleware) { a.httpServer.router.UseMiddleware(middlewares...) } -func (a *App) UseMongo(db datasource.Mongo) { - a.container.Mongo = db -} - // AddCronJob registers a cron job to the cron table, the schedule is in * * * * * (6 part) format // denoting minutes, hours, days, months and day of week respectively. func (a *App) AddCronJob(schedule, jobName string, job CronFunc) { diff --git a/pkg/gofr/gofr_test.go b/pkg/gofr/gofr_test.go index a20fc0aaa..9dae3df18 100644 --- a/pkg/gofr/gofr_test.go +++ b/pkg/gofr/gofr_test.go @@ -7,6 +7,8 @@ import ( "io" "net/http" "net/http/httptest" + "os" + "path/filepath" "strconv" "testing" "time" @@ -333,7 +335,7 @@ func Test_initTracer(t *testing.T) { }{ {"zipkin exporter", mockConfig1, "Exporting traces to zipkin."}, {"jaeger exporter", mockConfig2, "Exporting traces to jaeger."}, - {"gofr exporter", mockConfig3, "Exporting traces to gofr at https://tracer.gofr.dev"}, + {"gofr exporter", mockConfig3, "Exporting traces to GoFr at https://tracer.gofr.dev"}, } for _, tc := range tests { @@ -389,6 +391,7 @@ func Test_UseMiddleware(t *testing.T) { port: 8001, }, container: c, + Config: config.NewMockConfig(map[string]string{"REQUEST_TIMEOUT": "5"}), } app.UseMiddleware(testMiddleware) @@ -409,7 +412,7 @@ func Test_UseMiddleware(t *testing.T) { resp, err := netClient.Do(req) if err != nil { - t.Errorf("error while making http request in Test_UseMiddleware. err : %v", err) + t.Errorf("error while making HTTP request in Test_UseMiddleware. err : %v", err) return } @@ -422,6 +425,50 @@ func Test_UseMiddleware(t *testing.T) { assert.Equal(t, "applied", testHeaderValue, "Test_UseMiddleware Failed! header value mismatch.") } +func Test_SwaggerEndpoints(t *testing.T) { + // Create the openapi.json file within the static directory + openAPIFilePath := filepath.Join("static", OpenAPIJSON) + + openAPIContent := []byte(`{"swagger": "2.0", "info": {"version": "1.0.0", "title": "Sample API"}}`) + if err := os.WriteFile(openAPIFilePath, openAPIContent, 0600); err != nil { + t.Errorf("Failed to create openapi.json file: %v", err) + return + } + + // Defer removal of swagger file from the static directory + defer func() { + if err := os.RemoveAll("static/openapi.json"); err != nil { + t.Errorf("Failed to remove swagger file from static directory: %v", err) + } + }() + + app := New() + app.httpRegistered = true + app.httpServer.port = 8002 + + go app.Run() + time.Sleep(1 * time.Second) + + var netClient = &http.Client{ + Timeout: time.Second * 5, + } + + re, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, + "http://localhost:8002"+"/.well-known/swagger", http.NoBody) + resp, err := netClient.Do(re) + + defer func() { + err = resp.Body.Close() + if err != nil { + t.Errorf("error closing response body: %v", err) + } + }() + + assert.Nil(t, err, "Expected error to be nil, got : %v", err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type")) +} + func Test_AddCronJob_Fail(t *testing.T) { a := App{container: &container.Container{}} stderr := testutil.StderrOutputForFunc(func() { diff --git a/pkg/gofr/grpc.go b/pkg/gofr/grpc.go index 0f9275074..939fea12b 100644 --- a/pkg/gofr/grpc.go +++ b/pkg/gofr/grpc.go @@ -31,16 +31,16 @@ func newGRPCServer(c *container.Container, port int) *grpcServer { func (g *grpcServer) Run(c *container.Container) { addr := ":" + strconv.Itoa(g.port) - c.Logger.Infof("starting grpc server at %s", addr) + c.Logger.Infof("starting gRPC server at %s", addr) listener, err := net.Listen("tcp", addr) if err != nil { - c.Logger.Errorf("error in starting grpc server at %s: %s", addr, err) + c.Logger.Errorf("error in starting gRPC server at %s: %s", addr, err) return } if err := g.server.Serve(listener); err != nil { - c.Logger.Errorf("error in starting grpc server at %s: %s", addr, err) + c.Logger.Errorf("error in starting gRPC server at %s: %s", addr, err) return } } diff --git a/pkg/gofr/grpc_test.go b/pkg/gofr/grpc_test.go index 64fe8a2e7..d4256d27b 100644 --- a/pkg/gofr/grpc_test.go +++ b/pkg/gofr/grpc_test.go @@ -28,8 +28,8 @@ func TestGRPC_ServerRun(t *testing.T) { port int expLog string }{ - {"net.Listen() error", nil, 99999, "error in starting grpc server"}, - {"server.Serve() error", new(grpc.Server), 10000, "error in starting grpc server"}, + {"net.Listen() error", nil, 99999, "error in starting gRPC server"}, + {"server.Serve() error", new(grpc.Server), 10000, "error in starting gRPC server"}, } for i, tc := range testCases { diff --git a/pkg/gofr/handler.go b/pkg/gofr/handler.go index 89ee7b59b..057881d9c 100644 --- a/pkg/gofr/handler.go +++ b/pkg/gofr/handler.go @@ -1,7 +1,11 @@ package gofr import ( + "context" + "errors" "os" + "strconv" + "time" "gofr.dev/pkg/gofr/container" gofrHTTP "gofr.dev/pkg/gofr/http" @@ -11,6 +15,8 @@ import ( "net/http" ) +const defaultRequestTimeout = 5 + type Handler func(c *Context) (interface{}, error) /* @@ -23,18 +29,50 @@ There is another possibility where we write our own Router implementation and le use that router which will return a Handler and httpServer will then create the context with injecting container and call that Handler with the new context. A similar implementation is done in CMD. Since this will require us to write our own router - we are not taking that path -for now. In the future, this can be considered as well if we are writing our own http router. +for now. In the future, this can be considered as well if we are writing our own HTTP router. */ type handler struct { - function Handler - container *container.Container + function Handler + container *container.Container + requestTimeout string } func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c := newContext(gofrHTTP.NewResponder(w, r.Method), gofrHTTP.NewRequest(r), h.container) - defer c.Trace("gofr-handler").End() - c.responder.Respond(h.function(c)) + + reqTimeout := h.setContextTimeout(h.requestTimeout) + + ctx, cancel := context.WithTimeout(r.Context(), time.Duration(reqTimeout)*time.Second) + defer cancel() + + c.Context = ctx + + done := make(chan struct{}) + + var ( + result interface{} + err error + ) + + go func() { + // Execute the handler function + result, err = h.function(c) + + close(done) + }() + + select { + case <-ctx.Done(): + // If the context's deadline has been exceeded, return a timeout error response + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + http.Error(w, "Request timed out", http.StatusRequestTimeout) + return + } + case <-done: + // Handler function completed + c.responder.Respond(result, err) + } } func healthHandler(c *Context) (interface{}, error) { @@ -60,5 +98,17 @@ func faviconHandler(*Context) (interface{}, error) { } func catchAllHandler(*Context) (interface{}, error) { - return nil, http.ErrMissingFile + return nil, gofrHTTP.ErrorInvalidRoute{} +} + +// Helper function to parse and validate request timeout. +func (h handler) setContextTimeout(timeout string) int { + reqTimeout, err := strconv.Atoi(timeout) + if err != nil || reqTimeout < 0 { + h.container.Error("invalid value of config REQUEST_TIMEOUT. setting default value to 5 seconds.") + + reqTimeout = defaultRequestTimeout + } + + return reqTimeout } diff --git a/pkg/gofr/handler_test.go b/pkg/gofr/handler_test.go index 690652907..6ccf220a2 100644 --- a/pkg/gofr/handler_test.go +++ b/pkg/gofr/handler_test.go @@ -8,6 +8,7 @@ import ( "net/http/httptest" "os" "testing" + "time" "github.com/stretchr/testify/assert" @@ -59,6 +60,25 @@ func TestHandler_ServeHTTP(t *testing.T) { } } +func TestHandler_ServeHTTP_Timeout(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/", http.NoBody) + + h := handler{requestTimeout: "1"} + + h.container = &container.Container{Logger: logging.NewLogger(logging.FATAL)} + h.function = func(*Context) (interface{}, error) { + time.Sleep(2 * time.Second) + + return "hey", nil + } + + h.ServeHTTP(w, r) + + assert.Equal(t, http.StatusRequestTimeout, w.Code, "TestHandler_ServeHTTP_Timeout Failed") + assert.Equal(t, "Request timed out\n", w.Body.String(), "TestHandler_ServeHTTP_Timeout Failed") +} + func TestHandler_faviconHandlerError(t *testing.T) { c := Context{ Context: context.Background(), @@ -120,7 +140,7 @@ func TestHandler_catchAllHandler(t *testing.T) { assert.Equal(t, data, nil, "TEST Failed.\n") - assert.Equal(t, http.ErrMissingFile, err, "TEST Failed.\n") + assert.Equal(t, gofrHTTP.ErrorInvalidRoute{}, err, "TEST Failed.\n") } func TestHandler_livelinessHandler(t *testing.T) { diff --git a/pkg/gofr/http/errors.go b/pkg/gofr/http/errors.go new file mode 100644 index 000000000..951515db5 --- /dev/null +++ b/pkg/gofr/http/errors.go @@ -0,0 +1,60 @@ +// Package http provides a set of utilities for handling HTTP requests and responses within the GoFr framework. +package http + +import ( + "fmt" + "net/http" + "strings" +) + +// ErrorEntityNotFound represents an error for when an entity is not found in the system. +type ErrorEntityNotFound struct { + Name string + Value string +} + +func (e ErrorEntityNotFound) Error() string { + // For ex: "No entity found with id: 2" + return fmt.Sprintf("No entity found with %s: %s", e.Name, e.Value) +} + +func (e ErrorEntityNotFound) StatusCode() int { + return http.StatusNotFound +} + +// ErrorInvalidParam represents an error for invalid parameter values. +type ErrorInvalidParam struct { + Params []string `json:"param,omitempty"` // Params contains the list of invalid parameter names. +} + +func (e ErrorInvalidParam) Error() string { + return fmt.Sprintf("'%d' invalid parameter(s): %s", len(e.Params), strings.Join(e.Params, ", ")) +} + +func (e ErrorInvalidParam) StatusCode() int { + return http.StatusBadRequest +} + +// ErrorMissingParam represents an error for missing parameters in a request. +type ErrorMissingParam struct { + Params []string `json:"param,omitempty"` +} + +func (e ErrorMissingParam) Error() string { + return fmt.Sprintf("'%d' missing parameter(s): %s", len(e.Params), strings.Join(e.Params, ", ")) +} + +func (e ErrorMissingParam) StatusCode() int { + return http.StatusBadRequest +} + +// ErrorInvalidRoute represents an error for invalid route in a request. +type ErrorInvalidRoute struct{} + +func (e ErrorInvalidRoute) Error() string { + return "route not registered" +} + +func (e ErrorInvalidRoute) StatusCode() int { + return http.StatusNotFound +} diff --git a/pkg/gofr/http/errors_test.go b/pkg/gofr/http/errors_test.go new file mode 100644 index 000000000..cd688a94a --- /dev/null +++ b/pkg/gofr/http/errors_test.go @@ -0,0 +1,84 @@ +package http + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestErrorEntityNotFound(t *testing.T) { + fieldName := "id" + fieldValue := "2" + + err := ErrorEntityNotFound{Name: fieldName, Value: fieldValue} + expectedMsg := fmt.Sprintf("No entity found with %s: %s", fieldName, fieldValue) + + assert.Equal(t, expectedMsg, err.Error(), "TEST Failed.\n") +} + +func TestErrorEntityNotFound_StatusCode(t *testing.T) { + err := ErrorEntityNotFound{} + expectedCode := http.StatusNotFound + + assert.Equal(t, expectedCode, err.StatusCode(), "TEST Failed.\n") +} + +func TestErrorInvalidParam(t *testing.T) { + tests := []struct { + desc string + params []string + expectedMsg string + }{ + {"no parameter", make([]string, 0), "'0' invalid parameter(s): "}, + {"single parameter", []string{"uuid"}, "'1' invalid parameter(s): uuid"}, + {"list of params", []string{"id", "name", "age"}, "'3' invalid parameter(s): id, name, age"}, + } + + for i, tc := range tests { + err := ErrorInvalidParam{Params: tc.params} + + assert.Equal(t, tc.expectedMsg, err.Error(), "TEST[%d], Failed.\n%s", i, tc.desc) + } +} + +func TestInvalidParameter_StatusCode(t *testing.T) { + err := ErrorInvalidParam{} + expectedCode := http.StatusBadRequest + + assert.Equal(t, expectedCode, err.StatusCode(), "TestErrorInvalidParam_StatusCode Failed!") +} + +func TestErrorMissingParam(t *testing.T) { + tests := []struct { + desc string + params []string + expectedMsg string + }{ + {"no parameter", make([]string, 0), "'0' missing parameter(s): "}, + {"single parameter", []string{"uuid"}, "'1' missing parameter(s): uuid"}, + {"list of params", []string{"id", "name", "age"}, "'3' missing parameter(s): id, name, age"}, + } + + for i, tc := range tests { + err := ErrorMissingParam{Params: tc.params} + + assert.Equal(t, tc.expectedMsg, err.Error(), "TEST[%d], Failed.\n%s", i, tc.desc) + } +} + +func TestMissingParameter_StatusCode(t *testing.T) { + err := ErrorMissingParam{} + expectedCode := http.StatusBadRequest + + assert.Equal(t, expectedCode, err.StatusCode(), "TEST Failed.\n") +} + +func TestErrorInvalidRoute(t *testing.T) { + err := ErrorInvalidRoute{} + + assert.Equal(t, "route not registered", err.Error(), "TEST Failed.\n") + + assert.Equal(t, http.StatusNotFound, err.StatusCode(), "TEST Failed.\n") +} diff --git a/pkg/gofr/http/metrics.go b/pkg/gofr/http/metrics.go index 4e380447d..32409dec7 100644 --- a/pkg/gofr/http/metrics.go +++ b/pkg/gofr/http/metrics.go @@ -1,4 +1,3 @@ -// Package http provides a set of utilities for handling HTTP requests and responses within the GoFr framework. package http import "context" @@ -6,7 +5,4 @@ import "context" // Metrics represents an interface for registering the default metrics in GoFr framework. type Metrics interface { IncrementCounter(ctx context.Context, name string, labels ...string) - DeltaUpDownCounter(ctx context.Context, name string, value float64, labels ...string) - RecordHistogram(ctx context.Context, name string, value float64, labels ...string) - SetGauge(name string, value float64, labels ...string) } diff --git a/pkg/gofr/http/middleware/logger.go b/pkg/gofr/http/middleware/logger.go index d79bad327..fbfa0b968 100644 --- a/pkg/gofr/http/middleware/logger.go +++ b/pkg/gofr/http/middleware/logger.go @@ -88,8 +88,13 @@ func Logging(logger logger) func(inner http.Handler) http.Handler { URI: req.RequestURI, Response: res.status, } + if logger != nil { - logger.Log(l) + if res.status >= http.StatusInternalServerError { + logger.Error(l) + } else { + logger.Log(l) + } } }(srw, r) diff --git a/pkg/gofr/http/middleware/logger_test.go b/pkg/gofr/http/middleware/logger_test.go index 61a86e3f6..cdd974b8f 100644 --- a/pkg/gofr/http/middleware/logger_test.go +++ b/pkg/gofr/http/middleware/logger_test.go @@ -55,12 +55,32 @@ func Test_LoggingMiddleware(t *testing.T) { assert.Contains(t, logs, "GET 200") } +func Test_LoggingMiddlewareError(t *testing.T) { + logs := testutil.StderrOutputForFunc(func() { + req, _ := http.NewRequestWithContext(context.Background(), "GET", "http://dummy", http.NoBody) + + rr := httptest.NewRecorder() + + handler := Logging(logging.NewMockLogger(logging.ERROR))(http.HandlerFunc(testHandlerError)) + + handler.ServeHTTP(rr, req) + }) + + assert.Contains(t, logs, "GET 500") +} + // Test handler that uses the middleware. func testHandler(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("Test Handler")) } +// Test handler for internalServerErrors that uses the middleware. +func testHandlerError(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("error")) +} + func Test_LoggingMiddlewareStringPanicHandling(t *testing.T) { logs := testutil.StderrOutputForFunc(func() { req, _ := http.NewRequestWithContext(context.Background(), "GET", "http://dummy", http.NoBody) diff --git a/pkg/gofr/http/request_test.go b/pkg/gofr/http/request_test.go index 18c36c79a..4b6c04777 100644 --- a/pkg/gofr/http/request_test.go +++ b/pkg/gofr/http/request_test.go @@ -73,7 +73,7 @@ func TestBind_FileSuccess(t *testing.T) { assert.Equal(t, "Hello! This is file A.\n", string(x.Zip.Files["a.txt"].Bytes())) assert.Equal(t, "Hello! This is file B.\n\n", string(x.Zip.Files["b.txt"].Bytes())) - // Assert zip file bind for pinter + // Assert zip file bind for pointer assert.NotNil(t, x.ZipPtr) assert.Equal(t, 2, len(x.ZipPtr.Files)) assert.Equal(t, "Hello! This is file A.\n", string(x.ZipPtr.Files["a.txt"].Bytes())) diff --git a/pkg/gofr/http/responder.go b/pkg/gofr/http/responder.go index 92c62790e..044b7f5bb 100644 --- a/pkg/gofr/http/responder.go +++ b/pkg/gofr/http/responder.go @@ -2,7 +2,6 @@ package http import ( "encoding/json" - "errors" "net/http" resTypes "gofr.dev/pkg/gofr/http/response" @@ -62,8 +61,9 @@ func (r Responder) HTTPStatusFromError(err error) (status int, errObj interface{ } } - if errors.Is(err, http.ErrMissingFile) { - return http.StatusNotFound, map[string]interface{}{ + e, ok := err.(statusCodeResponder) + if ok { + return e.StatusCode(), map[string]interface{}{ "message": err.Error(), } } @@ -78,3 +78,7 @@ type response struct { Error interface{} `json:"error,omitempty"` Data interface{} `json:"data,omitempty"` } + +type statusCodeResponder interface { + StatusCode() int +} diff --git a/pkg/gofr/http/responder_test.go b/pkg/gofr/http/responder_test.go index 13e6907c6..6d00b8e54 100644 --- a/pkg/gofr/http/responder_test.go +++ b/pkg/gofr/http/responder_test.go @@ -33,6 +33,7 @@ func TestResponder_Respond(t *testing.T) { func TestResponder_HTTPStatusFromError(t *testing.T) { r := NewResponder(httptest.NewRecorder(), http.MethodGet) + errInvalidParam := ErrorInvalidParam{Params: []string{"name"}} tests := []struct { desc string @@ -41,10 +42,12 @@ func TestResponder_HTTPStatusFromError(t *testing.T) { errObj interface{} }{ {"success case", nil, http.StatusOK, nil}, - {"file not found", http.ErrMissingFile, http.StatusNotFound, map[string]interface{}{ - "message": http.ErrMissingFile.Error()}}, + {"file not found", ErrorInvalidRoute{}, http.StatusNotFound, map[string]interface{}{ + "message": ErrorInvalidRoute{}.Error()}}, {"internal server error", http.ErrHandlerTimeout, http.StatusInternalServerError, map[string]interface{}{"message": http.ErrHandlerTimeout.Error()}}, + {"invalid parameters error", &errInvalidParam, http.StatusBadRequest, + map[string]interface{}{"message": errInvalidParam.Error()}}, } for i, tc := range tests { diff --git a/pkg/gofr/metrics/errors.go b/pkg/gofr/metrics/errors.go index 37f221d83..bac7e5e7a 100644 --- a/pkg/gofr/metrics/errors.go +++ b/pkg/gofr/metrics/errors.go @@ -1,4 +1,4 @@ -// Package metrics provides functionalities for instrumenting Gofr applications with metrics. +// Package metrics provides functionalities for instrumenting GoFr applications with metrics. package metrics import "fmt" diff --git a/pkg/gofr/metrics/register_test.go b/pkg/gofr/metrics/register_test.go index 3381a9dc6..d940a74a8 100644 --- a/pkg/gofr/metrics/register_test.go +++ b/pkg/gofr/metrics/register_test.go @@ -153,7 +153,7 @@ func Test_NewMetricsManagerLabelHighCardinality(t *testing.T) { metrics.IncrementCounter(context.Background(), "counter-test", "label1", "value1", "label2", "value2", "label3", "value3", "label4", "value4", "label5", "value5", "label6", "value6", - "label7", "value7", "label8", "value8", "label9", "value9", "label10", "value10", "label11", "valu11", "label12", "value12") + "label7", "value7", "label8", "value8", "label9", "value9", "label10", "value10", "label11", "value11", "label12", "value12") } log := testutil.StdoutOutputForFunc(logs) diff --git a/pkg/gofr/migration/migration_test.go b/pkg/gofr/migration/migration_test.go index ba09d7fb2..2131961f6 100644 --- a/pkg/gofr/migration/migration_test.go +++ b/pkg/gofr/migration/migration_test.go @@ -45,11 +45,11 @@ func TestMigration_NoDatasource(t *testing.T) { func Test_getMigratorDBInitialisation(t *testing.T) { cntnr, _ := container.NewMockContainer(t) - datasource, _, isIntialised := getMigrator(cntnr) + datasource, _, isInitialised := getMigrator(cntnr) assert.NotNil(t, datasource.SQL, "TEST Failed \nSQL not initialized, but should have been initialized") assert.NotNil(t, datasource.Redis, "TEST Failed \nRedis not initialized, but should have been initialized") - assert.Equal(t, true, isIntialised, "TEST Failed \nNo datastores are Initialized") + assert.Equal(t, true, isInitialised, "TEST Failed \nNo datastores are Initialized") } func Test_getMigratorDatastoreNotInitialised(t *testing.T) { @@ -58,7 +58,7 @@ func Test_getMigratorDatastoreNotInitialised(t *testing.T) { container.SQL = nil container.Redis = nil - datasource, _, isIntialised := getMigrator(container) + datasource, _, isInitialised := getMigrator(container) datasource.rollback(container, migrationData{}) @@ -67,7 +67,7 @@ func Test_getMigratorDatastoreNotInitialised(t *testing.T) { assert.Equal(t, migrationData{}, datasource.beginTransaction(container), "TEST Failed") assert.Nil(t, datasource.commitMigration(container, migrationData{}), "TEST Failed") - assert.Equal(t, false, isIntialised, "TEST Failed \nDatastores are Initialized") + assert.Equal(t, false, isInitialised, "TEST Failed \nDatastores are Initialized") }) assert.Contains(t, logs, "Migration 0 ran successfully", "TEST Failed") diff --git a/pkg/gofr/migration/sql.go b/pkg/gofr/migration/sql.go index 69d2e63da..ef3802ad0 100644 --- a/pkg/gofr/migration/sql.go +++ b/pkg/gofr/migration/sql.go @@ -113,7 +113,7 @@ func (d sqlMigrator) getLastMigration(c *container.Container) int64 { func (d sqlMigrator) commitMigration(c *container.Container, data migrationData) error { switch c.SQL.Dialect() { - case "mysql": + case "mysql", "sqlite": err := insertMigrationRecord(data.SQLTx, insertGoFrMigrationRowMySQL, data.MigrationNumber, data.StartTime) if err != nil { return err diff --git a/pkg/gofr/responder.go b/pkg/gofr/responder.go index 12fc15481..465033b86 100644 --- a/pkg/gofr/responder.go +++ b/pkg/gofr/responder.go @@ -1,7 +1,7 @@ package gofr // Responder is used by the application to provide output. This is implemented for both -// cmd and http server application. +// cmd and HTTP server application. type Responder interface { Respond(data interface{}, err error) } diff --git a/pkg/gofr/service/apikey_auth.go b/pkg/gofr/service/apikey_auth.go index 9fc7bfc36..3682551ea 100644 --- a/pkg/gofr/service/apikey_auth.go +++ b/pkg/gofr/service/apikey_auth.go @@ -12,70 +12,70 @@ type APIKeyConfig struct { } func (a *APIKeyConfig) AddOption(h HTTP) HTTP { - return &APIKeyAuthProvider{ + return &apiKeyAuthProvider{ apiKey: a.APIKey, HTTP: h, } } -type APIKeyAuthProvider struct { +type apiKeyAuthProvider struct { apiKey string HTTP } -func (a *APIKeyAuthProvider) Get(ctx context.Context, path string, queryParams map[string]interface{}) (*http.Response, error) { +func (a *apiKeyAuthProvider) Get(ctx context.Context, path string, queryParams map[string]interface{}) (*http.Response, error) { return a.GetWithHeaders(ctx, path, queryParams, nil) } -func (a *APIKeyAuthProvider) GetWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, +func (a *apiKeyAuthProvider) GetWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, headers map[string]string) (*http.Response, error) { headers = setXApiKey(headers, a.apiKey) return a.HTTP.GetWithHeaders(ctx, path, queryParams, headers) } -func (a *APIKeyAuthProvider) Post(ctx context.Context, path string, queryParams map[string]interface{}, +func (a *apiKeyAuthProvider) Post(ctx context.Context, path string, queryParams map[string]interface{}, body []byte) (*http.Response, error) { return a.PostWithHeaders(ctx, path, queryParams, body, nil) } -func (a *APIKeyAuthProvider) PostWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte, +func (a *apiKeyAuthProvider) PostWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte, headers map[string]string) (*http.Response, error) { headers = setXApiKey(headers, a.apiKey) return a.HTTP.PostWithHeaders(ctx, path, queryParams, body, headers) } -func (a *APIKeyAuthProvider) Put(ctx context.Context, api string, queryParams map[string]interface{}, body []byte) ( +func (a *apiKeyAuthProvider) Put(ctx context.Context, api string, queryParams map[string]interface{}, body []byte) ( *http.Response, error) { return a.PutWithHeaders(ctx, api, queryParams, body, nil) } -func (a *APIKeyAuthProvider) PutWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte, +func (a *apiKeyAuthProvider) PutWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte, headers map[string]string) (*http.Response, error) { headers = setXApiKey(headers, a.apiKey) return a.HTTP.PutWithHeaders(ctx, path, queryParams, body, headers) } -func (a *APIKeyAuthProvider) Patch(ctx context.Context, path string, queryParams map[string]interface{}, body []byte) ( +func (a *apiKeyAuthProvider) Patch(ctx context.Context, path string, queryParams map[string]interface{}, body []byte) ( *http.Response, error) { return a.PatchWithHeaders(ctx, path, queryParams, body, nil) } -func (a *APIKeyAuthProvider) PatchWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte, +func (a *apiKeyAuthProvider) PatchWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte, headers map[string]string) (*http.Response, error) { headers = setXApiKey(headers, a.apiKey) return a.HTTP.PatchWithHeaders(ctx, path, queryParams, body, headers) } -func (a *APIKeyAuthProvider) Delete(ctx context.Context, path string, body []byte) (*http.Response, error) { +func (a *apiKeyAuthProvider) Delete(ctx context.Context, path string, body []byte) (*http.Response, error) { return a.DeleteWithHeaders(ctx, path, body, nil) } -func (a *APIKeyAuthProvider) DeleteWithHeaders(ctx context.Context, path string, body []byte, headers map[string]string) ( +func (a *apiKeyAuthProvider) DeleteWithHeaders(ctx context.Context, path string, body []byte, headers map[string]string) ( *http.Response, error) { headers = setXApiKey(headers, a.apiKey) diff --git a/pkg/gofr/service/basic_auth.go b/pkg/gofr/service/basic_auth.go index 0503f4c73..9ebc1c1a5 100644 --- a/pkg/gofr/service/basic_auth.go +++ b/pkg/gofr/service/basic_auth.go @@ -12,21 +12,21 @@ type BasicAuthConfig struct { } func (a *BasicAuthConfig) AddOption(h HTTP) HTTP { - return &BasicAuthProvider{ + return &basicAuthProvider{ userName: a.UserName, password: a.Password, HTTP: h, } } -type BasicAuthProvider struct { +type basicAuthProvider struct { userName string password string HTTP } -func (ba *BasicAuthProvider) addAuthorizationHeader(headers map[string]string) error { +func (ba *basicAuthProvider) addAuthorizationHeader(headers map[string]string) error { decodedPassword, err := b64.StdEncoding.DecodeString(ba.password) if err != nil { return err @@ -39,11 +39,11 @@ func (ba *BasicAuthProvider) addAuthorizationHeader(headers map[string]string) e return nil } -func (ba *BasicAuthProvider) Get(ctx context.Context, path string, queryParams map[string]interface{}) (*http.Response, error) { +func (ba *basicAuthProvider) Get(ctx context.Context, path string, queryParams map[string]interface{}) (*http.Response, error) { return ba.GetWithHeaders(ctx, path, queryParams, nil) } -func (ba *BasicAuthProvider) GetWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, +func (ba *basicAuthProvider) GetWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, headers map[string]string) (*http.Response, error) { err := ba.populateHeaders(headers) if err != nil { @@ -53,12 +53,12 @@ func (ba *BasicAuthProvider) GetWithHeaders(ctx context.Context, path string, qu return ba.HTTP.GetWithHeaders(ctx, path, queryParams, headers) } -func (ba *BasicAuthProvider) Post(ctx context.Context, path string, queryParams map[string]interface{}, +func (ba *basicAuthProvider) Post(ctx context.Context, path string, queryParams map[string]interface{}, body []byte) (*http.Response, error) { return ba.PostWithHeaders(ctx, path, queryParams, body, nil) } -func (ba *BasicAuthProvider) PostWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, +func (ba *basicAuthProvider) PostWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte, headers map[string]string) (*http.Response, error) { err := ba.populateHeaders(headers) if err != nil { @@ -68,11 +68,11 @@ func (ba *BasicAuthProvider) PostWithHeaders(ctx context.Context, path string, q return ba.HTTP.PostWithHeaders(ctx, path, queryParams, body, headers) } -func (ba *BasicAuthProvider) Put(ctx context.Context, api string, queryParams map[string]interface{}, body []byte) (*http.Response, error) { +func (ba *basicAuthProvider) Put(ctx context.Context, api string, queryParams map[string]interface{}, body []byte) (*http.Response, error) { return ba.PutWithHeaders(ctx, api, queryParams, body, nil) } -func (ba *BasicAuthProvider) PutWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, +func (ba *basicAuthProvider) PutWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte, headers map[string]string) (*http.Response, error) { err := ba.populateHeaders(headers) if err != nil { @@ -82,12 +82,12 @@ func (ba *BasicAuthProvider) PutWithHeaders(ctx context.Context, path string, qu return ba.HTTP.PutWithHeaders(ctx, path, queryParams, body, headers) } -func (ba *BasicAuthProvider) Patch(ctx context.Context, path string, queryParams map[string]interface{}, +func (ba *basicAuthProvider) Patch(ctx context.Context, path string, queryParams map[string]interface{}, body []byte) (*http.Response, error) { return ba.PatchWithHeaders(ctx, path, queryParams, body, nil) } -func (ba *BasicAuthProvider) PatchWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, +func (ba *basicAuthProvider) PatchWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte, headers map[string]string) (*http.Response, error) { err := ba.populateHeaders(headers) if err != nil { @@ -97,11 +97,11 @@ func (ba *BasicAuthProvider) PatchWithHeaders(ctx context.Context, path string, return ba.HTTP.PatchWithHeaders(ctx, path, queryParams, body, headers) } -func (ba *BasicAuthProvider) Delete(ctx context.Context, path string, body []byte) (*http.Response, error) { +func (ba *basicAuthProvider) Delete(ctx context.Context, path string, body []byte) (*http.Response, error) { return ba.DeleteWithHeaders(ctx, path, body, nil) } -func (ba *BasicAuthProvider) DeleteWithHeaders(ctx context.Context, path string, body []byte, +func (ba *basicAuthProvider) DeleteWithHeaders(ctx context.Context, path string, body []byte, headers map[string]string) (*http.Response, error) { err := ba.populateHeaders(headers) if err != nil { @@ -111,7 +111,7 @@ func (ba *BasicAuthProvider) DeleteWithHeaders(ctx context.Context, path string, return ba.HTTP.DeleteWithHeaders(ctx, path, body, headers) } -func (ba *BasicAuthProvider) populateHeaders(headers map[string]string) error { +func (ba *basicAuthProvider) populateHeaders(headers map[string]string) error { if headers == nil { headers = make(map[string]string) } diff --git a/pkg/gofr/service/basic_auth_test.go b/pkg/gofr/service/basic_auth_test.go index cf4289cbf..f7605764c 100644 --- a/pkg/gofr/service/basic_auth_test.go +++ b/pkg/gofr/service/basic_auth_test.go @@ -199,7 +199,7 @@ func checkAuthHeaders(r *http.Request, t *testing.T) { } func Test_addAuthorizationHeader_Error(t *testing.T) { - ba := &BasicAuthProvider{password: "invalid_password"} + ba := &basicAuthProvider{password: "invalid_password"} headers := make(map[string]string) err := ba.addAuthorizationHeader(headers) diff --git a/pkg/gofr/service/circuit_breaker.go b/pkg/gofr/service/circuit_breaker.go index 90ceed3cf..0398e0cf5 100644 --- a/pkg/gofr/service/circuit_breaker.go +++ b/pkg/gofr/service/circuit_breaker.go @@ -8,7 +8,7 @@ import ( "time" ) -// CircuitBreaker states. +// circuitBreaker states. const ( ClosedState = iota OpenState @@ -20,14 +20,14 @@ var ( ErrUnexpectedCircuitBreakerResultType = errors.New("unexpected result type from circuit breaker") ) -// CircuitBreakerConfig holds the configuration for the CircuitBreaker. +// CircuitBreakerConfig holds the configuration for the circuitBreaker. type CircuitBreakerConfig struct { Threshold int // Threshold represents the max no of retry before switching the circuit breaker state. Interval time.Duration // Interval represents the time interval duration between hitting the HealthURL } -// CircuitBreaker represents a circuit breaker implementation. -type CircuitBreaker struct { +// circuitBreaker represents a circuit breaker implementation. +type circuitBreaker struct { mu sync.RWMutex state int // ClosedState or OpenState failureCount int @@ -38,9 +38,11 @@ type CircuitBreaker struct { HTTP } -// NewCircuitBreaker creates a new CircuitBreaker instance based on the provided config. -func NewCircuitBreaker(config CircuitBreakerConfig, h HTTP) *CircuitBreaker { - cb := &CircuitBreaker{ +// NewCircuitBreaker creates a new circuitBreaker instance based on the provided config. +// +//nolint:revive // We do not want anyone using the circuit breaker without initialization steps. +func NewCircuitBreaker(config CircuitBreakerConfig, h HTTP) *circuitBreaker { + cb := &circuitBreaker{ state: ClosedState, threshold: config.Threshold, interval: config.Interval, @@ -54,7 +56,7 @@ func NewCircuitBreaker(config CircuitBreakerConfig, h HTTP) *CircuitBreaker { } // executeWithCircuitBreaker executes the given function with circuit breaker protection. -func (cb *CircuitBreaker) executeWithCircuitBreaker(ctx context.Context, f func(ctx context.Context) (*http.Response, +func (cb *circuitBreaker) executeWithCircuitBreaker(ctx context.Context, f func(ctx context.Context) (*http.Response, error)) (*http.Response, error) { cb.mu.Lock() defer cb.mu.Unlock() @@ -88,7 +90,7 @@ func (cb *CircuitBreaker) executeWithCircuitBreaker(ctx context.Context, f func( } // isOpen returns true if the circuit breaker is in the open state. -func (cb *CircuitBreaker) isOpen() bool { +func (cb *circuitBreaker) isOpen() bool { cb.mu.Lock() defer cb.mu.Unlock() @@ -96,14 +98,14 @@ func (cb *CircuitBreaker) isOpen() bool { } // healthCheck performs the health check for the circuit breaker. -func (cb *CircuitBreaker) healthCheck(ctx context.Context) bool { +func (cb *circuitBreaker) healthCheck(ctx context.Context) bool { resp := cb.HealthCheck(ctx) return resp.Status == serviceUp } // startHealthChecks initiates periodic health checks. -func (cb *CircuitBreaker) startHealthChecks() { +func (cb *circuitBreaker) startHealthChecks() { ticker := time.NewTicker(cb.interval) for range ticker.C { @@ -118,19 +120,19 @@ func (cb *CircuitBreaker) startHealthChecks() { } // openCircuit transitions the circuit breaker to the open state. -func (cb *CircuitBreaker) openCircuit() { +func (cb *circuitBreaker) openCircuit() { cb.state = OpenState cb.lastChecked = time.Now() } // resetCircuit transitions the circuit breaker to the closed state. -func (cb *CircuitBreaker) resetCircuit() { +func (cb *circuitBreaker) resetCircuit() { cb.state = ClosedState cb.failureCount = 0 } // handleFailure increments the failure count and opens the circuit if the threshold is reached. -func (cb *CircuitBreaker) handleFailure() { +func (cb *circuitBreaker) handleFailure() { cb.failureCount++ if cb.failureCount > cb.threshold { cb.openCircuit() @@ -138,7 +140,7 @@ func (cb *CircuitBreaker) handleFailure() { } // resetFailureCount resets the failure count to zero. -func (cb *CircuitBreaker) resetFailureCount() { +func (cb *circuitBreaker) resetFailureCount() { cb.failureCount = 0 } @@ -146,7 +148,7 @@ func (cb *CircuitBreakerConfig) AddOption(h HTTP) HTTP { return NewCircuitBreaker(*cb, h) } -func (cb *CircuitBreaker) tryCircuitRecovery() bool { +func (cb *circuitBreaker) tryCircuitRecovery() bool { if time.Since(cb.lastChecked) > cb.interval && cb.healthCheck(context.TODO()) { cb.resetCircuit() return true @@ -155,7 +157,7 @@ func (cb *CircuitBreaker) tryCircuitRecovery() bool { return false } -func (cb *CircuitBreaker) handleCircuitBreakerResult(result interface{}, err error) (*http.Response, error) { +func (cb *circuitBreaker) handleCircuitBreakerResult(result interface{}, err error) (*http.Response, error) { if err != nil { return nil, err } @@ -168,7 +170,7 @@ func (cb *CircuitBreaker) handleCircuitBreakerResult(result interface{}, err err return response, nil } -func (cb *CircuitBreaker) doRequest(ctx context.Context, method, path string, queryParams map[string]interface{}, +func (cb *circuitBreaker) doRequest(ctx context.Context, method, path string, queryParams map[string]interface{}, body []byte, headers map[string]string) (*http.Response, error) { if cb.isOpen() { if !cb.tryCircuitRecovery() { @@ -211,59 +213,59 @@ func (cb *CircuitBreaker) doRequest(ctx context.Context, method, path string, qu return resp, err } -func (cb *CircuitBreaker) GetWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, +func (cb *circuitBreaker) GetWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, headers map[string]string) (*http.Response, error) { return cb.doRequest(ctx, http.MethodGet, path, queryParams, nil, headers) } // PostWithHeaders is a wrapper for doRequest with the POST method and headers. -func (cb *CircuitBreaker) PostWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, +func (cb *circuitBreaker) PostWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte, headers map[string]string) (*http.Response, error) { return cb.doRequest(ctx, http.MethodPost, path, queryParams, body, headers) } // PatchWithHeaders is a wrapper for doRequest with the PATCH method and headers. -func (cb *CircuitBreaker) PatchWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, +func (cb *circuitBreaker) PatchWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte, headers map[string]string) (*http.Response, error) { return cb.doRequest(ctx, http.MethodPatch, path, queryParams, body, headers) } // PutWithHeaders is a wrapper for doRequest with the PUT method and headers. -func (cb *CircuitBreaker) PutWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, +func (cb *circuitBreaker) PutWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte, headers map[string]string) (*http.Response, error) { return cb.doRequest(ctx, http.MethodPut, path, queryParams, body, headers) } // DeleteWithHeaders is a wrapper for doRequest with the DELETE method and headers. -func (cb *CircuitBreaker) DeleteWithHeaders(ctx context.Context, path string, body []byte, headers map[string]string) ( +func (cb *circuitBreaker) DeleteWithHeaders(ctx context.Context, path string, body []byte, headers map[string]string) ( *http.Response, error) { return cb.doRequest(ctx, http.MethodDelete, path, nil, body, headers) } -func (cb *CircuitBreaker) Get(ctx context.Context, path string, queryParams map[string]interface{}) (*http.Response, error) { +func (cb *circuitBreaker) Get(ctx context.Context, path string, queryParams map[string]interface{}) (*http.Response, error) { return cb.doRequest(ctx, http.MethodGet, path, queryParams, nil, nil) } // Post is a wrapper for doRequest with the POST method and headers. -func (cb *CircuitBreaker) Post(ctx context.Context, path string, queryParams map[string]interface{}, +func (cb *circuitBreaker) Post(ctx context.Context, path string, queryParams map[string]interface{}, body []byte) (*http.Response, error) { return cb.doRequest(ctx, http.MethodPost, path, queryParams, body, nil) } // Patch is a wrapper for doRequest with the PATCH method and headers. -func (cb *CircuitBreaker) Patch(ctx context.Context, path string, queryParams map[string]interface{}, +func (cb *circuitBreaker) Patch(ctx context.Context, path string, queryParams map[string]interface{}, body []byte) (*http.Response, error) { return cb.doRequest(ctx, http.MethodPatch, path, queryParams, body, nil) } // Put is a wrapper for doRequest with the PUT method and headers. -func (cb *CircuitBreaker) Put(ctx context.Context, path string, queryParams map[string]interface{}, +func (cb *circuitBreaker) Put(ctx context.Context, path string, queryParams map[string]interface{}, body []byte) (*http.Response, error) { return cb.doRequest(ctx, http.MethodPut, path, queryParams, body, nil) } // Delete is a wrapper for doRequest with the DELETE method and headers. -func (cb *CircuitBreaker) Delete(ctx context.Context, path string, body []byte) ( +func (cb *circuitBreaker) Delete(ctx context.Context, path string, body []byte) ( *http.Response, error) { return cb.doRequest(ctx, http.MethodDelete, path, nil, body, nil) } diff --git a/pkg/gofr/service/custom_header.go b/pkg/gofr/service/custom_header.go new file mode 100644 index 000000000..c61f03144 --- /dev/null +++ b/pkg/gofr/service/custom_header.go @@ -0,0 +1,93 @@ +package service + +import ( + "context" + "net/http" +) + +type DefaultHeaders struct { + Headers map[string]string +} + +func (a *DefaultHeaders) AddOption(h HTTP) HTTP { + return &customHeader{ + Headers: a.Headers, + HTTP: h, + } +} + +type customHeader struct { + Headers map[string]string + + HTTP +} + +func (a *customHeader) Get(ctx context.Context, path string, queryParams map[string]interface{}) (*http.Response, error) { + return a.GetWithHeaders(ctx, path, queryParams, nil) +} + +func (a *customHeader) GetWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, + headers map[string]string) (*http.Response, error) { + headers = setCustomHeader(headers, a.Headers) + + return a.HTTP.GetWithHeaders(ctx, path, queryParams, headers) +} + +func (a *customHeader) Post(ctx context.Context, path string, queryParams map[string]interface{}, + body []byte) (*http.Response, error) { + return a.PostWithHeaders(ctx, path, queryParams, body, nil) +} + +func (a *customHeader) PostWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte, + headers map[string]string) (*http.Response, error) { + headers = setCustomHeader(headers, a.Headers) + + return a.HTTP.PostWithHeaders(ctx, path, queryParams, body, headers) +} + +func (a *customHeader) Put(ctx context.Context, api string, queryParams map[string]interface{}, body []byte) ( + *http.Response, error) { + return a.PutWithHeaders(ctx, api, queryParams, body, nil) +} + +func (a *customHeader) PutWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte, + headers map[string]string) (*http.Response, error) { + headers = setCustomHeader(headers, a.Headers) + + return a.HTTP.PutWithHeaders(ctx, path, queryParams, body, headers) +} + +func (a *customHeader) Patch(ctx context.Context, path string, queryParams map[string]interface{}, body []byte) ( + *http.Response, error) { + return a.PatchWithHeaders(ctx, path, queryParams, body, nil) +} + +func (a *customHeader) PatchWithHeaders(ctx context.Context, path string, queryParams map[string]interface{}, body []byte, + headers map[string]string) (*http.Response, error) { + headers = setCustomHeader(headers, a.Headers) + + return a.HTTP.PatchWithHeaders(ctx, path, queryParams, body, headers) +} + +func (a *customHeader) Delete(ctx context.Context, path string, body []byte) (*http.Response, error) { + return a.DeleteWithHeaders(ctx, path, body, nil) +} + +func (a *customHeader) DeleteWithHeaders(ctx context.Context, path string, body []byte, headers map[string]string) ( + *http.Response, error) { + headers = setCustomHeader(headers, a.Headers) + + return a.HTTP.DeleteWithHeaders(ctx, path, body, headers) +} + +func setCustomHeader(headers, customHeader map[string]string) map[string]string { + if headers == nil { + headers = make(map[string]string) + } + + for key, value := range customHeader { + headers[key] = value + } + + return headers +} diff --git a/pkg/gofr/service/custom_header_test.go b/pkg/gofr/service/custom_header_test.go new file mode 100644 index 000000000..bc2edf597 --- /dev/null +++ b/pkg/gofr/service/custom_header_test.go @@ -0,0 +1,168 @@ +package service + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "gofr.dev/pkg/gofr/logging" +) + +func Test_CustomDomainProvider_Get(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + queryParams := map[string]interface{}{"key": "value"} + body := []byte("body") + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusOK) + + _, err := w.Write(body) + if err != nil { + return + } + })) + defer server.Close() + + customHeaderService := NewHTTPService(server.URL, logging.NewMockLogger(logging.INFO), nil, + &DefaultHeaders{ + Headers: map[string]string{ + "TEST_KEY": "test_value", + }, + }) + + resp, err := customHeaderService.Get(context.Background(), "/path", queryParams) + assert.Nil(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Nil(t, err) + + bodyBytes, _ := io.ReadAll(resp.Body) + + assert.Equal(t, string(body), string(bodyBytes)) +} + +func Test_CustomDomainProvider_Post(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + queryParams := map[string]interface{}{"key": "value"} + body := []byte("body") + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + + w.WriteHeader(http.StatusCreated) + })) + defer server.Close() + + customHeaderService := NewHTTPService(server.URL, logging.NewMockLogger(logging.INFO), nil, + &DefaultHeaders{ + Headers: map[string]string{ + "TEST_KEY": "test_value", + }}) + + resp, err := customHeaderService.Post(context.Background(), "/path", queryParams, body) + assert.Nil(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusCreated, resp.StatusCode) + assert.Nil(t, err) +} + +func TestCustomDomainProvider_Put(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + queryParams := map[string]interface{}{"key": "value"} + body := []byte("body") + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method) + + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + customHeaderService := NewHTTPService(server.URL, logging.NewMockLogger(logging.INFO), nil, + &DefaultHeaders{ + Headers: map[string]string{ + "TEST_KEY": "test_value", + }}) + + resp, err := customHeaderService.Put(context.Background(), "/path", queryParams, body) + assert.Nil(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Nil(t, err) +} + +func TestCustomDomainProvider_Patch(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + queryParams := map[string]interface{}{"key": "value"} + body := []byte("body") + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPatch, r.Method) + + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + customHeaderService := NewHTTPService(server.URL, logging.NewMockLogger(logging.INFO), nil, + &DefaultHeaders{ + Headers: map[string]string{ + "TEST_KEY": "test_value", + }}) + + resp, err := customHeaderService.Patch(context.Background(), "/path", queryParams, body) + assert.Nil(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Nil(t, err) +} + +func TestCustomDomainProvider_Delete(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + body := []byte("body") + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodDelete, r.Method) + + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + customHeaderService := NewHTTPService(server.URL, logging.NewMockLogger(logging.INFO), nil, + &DefaultHeaders{ + Headers: map[string]string{ + "TEST_KEY": "test_value", + }}) + + resp, err := customHeaderService.Delete(context.Background(), "/path", body) + assert.Nil(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusNoContent, resp.StatusCode) + assert.Nil(t, err) +} diff --git a/pkg/gofr/service/logger.go b/pkg/gofr/service/logger.go index 38e964140..53380f843 100644 --- a/pkg/gofr/service/logger.go +++ b/pkg/gofr/service/logger.go @@ -20,7 +20,7 @@ type Log struct { } func (l *Log) PrettyPrint(writer io.Writer) { - fmt.Fprintf(writer, "\u001B[38;5;8m%s \u001B[38;5;%dm%d\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s %s \n", + fmt.Fprintf(writer, "\u001B[38;5;8m%s \u001B[38;5;%dm%-6d\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s %s \n", l.CorrelationID, colorForStatusCode(l.ResponseCode), l.ResponseCode, l.ResponseTime, l.HTTPMethod, l.URI) } diff --git a/pkg/gofr/service/logger_test.go b/pkg/gofr/service/logger_test.go index a1be92a2f..07d8c1ae1 100644 --- a/pkg/gofr/service/logger_test.go +++ b/pkg/gofr/service/logger_test.go @@ -20,7 +20,7 @@ func TestLog_PrettyPrint(t *testing.T) { l.PrettyPrint(w) - assert.Equal(t, "\u001B[38;5;8mabc-test-correlation-id \u001B[38;5;34m200\u001B[0m 100\u001B[38;5;8mµs\u001B[0m GET /api/test \n", + assert.Equal(t, "\u001B[38;5;8mabc-test-correlation-id \u001B[38;5;34m200 \u001B[0m 100\u001B[38;5;8mµs\u001B[0m GET /api/test \n", w.String()) } diff --git a/pkg/gofr/service/new.go b/pkg/gofr/service/new.go index 096739473..1d93666e1 100644 --- a/pkg/gofr/service/new.go +++ b/pkg/gofr/service/new.go @@ -67,7 +67,7 @@ type httpClient interface { // It initializes the http.Client, url, Tracer, and Logger fields of the httpService struct with the provided values. func NewHTTPService(serviceAddress string, logger Logger, metrics Metrics, options ...Options) HTTP { h := &httpService{ - // using default http client to do http communication + // using default HTTP client to do HTTP communication Client: &http.Client{}, url: serviceAddress, Tracer: otel.Tracer("gofr-http-client"), diff --git a/pkg/gofr/version/version.go b/pkg/gofr/version/version.go index bd9d052d5..850a8cb25 100644 --- a/pkg/gofr/version/version.go +++ b/pkg/gofr/version/version.go @@ -1,3 +1,3 @@ package version -const Framework = "v1.6.1" +const Framework = "v1.7.0"