Repository: labstack/echo (go)

Issue:
Log level to output stack trace of recover middleware
### Issue Description

I want to set the output log level for stack trace.

### Checklist

- [x] Dependencies installed
- [x] No typos
- [x] Searched existing issues and docs

### Expected behaviour

I want to set the output log level for stack trace.

### Actual behaviour

Only output with `Print`.

### Steps to reproduce

https://github.com/labstack/echo/blob/6e7c7cea03c4ac9d551c98d92fc4168888b8e2d5/middleware/recover_test.go

### Version/commit

v4.1.16

Pull request description:
Fix recover print stack trace log level
Fixed to be able to set the log level of stack trace when `recover`.

Fixes #1605.

Patch:
diff --git a/middleware/recover.go b/middleware/recover.go
index e87aaf3..0dbe740 100644
--- a/middleware/recover.go
+++ b/middleware/recover.go
@@ -5,6 +5,7 @@ import (
 	"runtime"
 
 	"github.com/labstack/echo/v4"
+	"github.com/labstack/gommon/log"
 )
 
 type (
@@ -25,6 +26,10 @@ type (
 		// DisablePrintStack disables printing stack trace.
 		// Optional. Default value as false.
 		DisablePrintStack bool `yaml:"disable_print_stack"`
+
+		// LogLevel is log level to printing stack trace.
+		// Optional. Default value 0 (Print).
+		LogLevel log.Lvl
 	}
 )
 
@@ -35,6 +40,7 @@ var (
 		StackSize:         4 << 10, // 4 KB
 		DisableStackAll:   false,
 		DisablePrintStack: false,
+		LogLevel:          0,
 	}
 )
 
@@ -70,7 +76,21 @@ func RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc {
 					stack := make([]byte, config.StackSize)
 					length := runtime.Stack(stack, !config.DisableStackAll)
 					if !config.DisablePrintStack {
-						c.Logger().Printf("[PANIC RECOVER] %v %s\n", err, stack[:length])
+						msg := fmt.Sprintf("[PANIC RECOVER] %v %s\n", err, stack[:length])
+						switch config.LogLevel {
+						case log.DEBUG:
+							c.Logger().Debug(msg)
+						case log.INFO:
+							c.Logger().Info(msg)
+						case log.WARN:
+							c.Logger().Warn(msg)
+						case log.ERROR:
+							c.Logger().Error(msg)
+						case log.OFF:
+							// None.
+						default:
+							c.Logger().Print(msg)
+						}
 					}
 					c.Error(err)
 				}


Test patch:
diff --git a/middleware/recover_test.go b/middleware/recover_test.go
index 37707c5..6443329 100644
--- a/middleware/recover_test.go
+++ b/middleware/recover_test.go
@@ -2,11 +2,13 @@ package middleware
 
 import (
 	"bytes"
+	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
 	"github.com/labstack/echo/v4"
+	"github.com/labstack/gommon/log"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -24,3 +26,58 @@ func TestRecover(t *testing.T) {
 	assert.Equal(t, http.StatusInternalServerError, rec.Code)
 	assert.Contains(t, buf.String(), "PANIC RECOVER")
 }
+
+func TestRecoverWithConfig_LogLevel(t *testing.T) {
+	tests := []struct {
+		logLevel  log.Lvl
+		levelName string
+	}{{
+		logLevel:  log.DEBUG,
+		levelName: "DEBUG",
+	}, {
+		logLevel:  log.INFO,
+		levelName: "INFO",
+	}, {
+		logLevel:  log.WARN,
+		levelName: "WARN",
+	}, {
+		logLevel:  log.ERROR,
+		levelName: "ERROR",
+	}, {
+		logLevel:  log.OFF,
+		levelName: "OFF",
+	}}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.levelName, func(t *testing.T) {
+			e := echo.New()
+			e.Logger.SetLevel(log.DEBUG)
+
+			buf := new(bytes.Buffer)
+			e.Logger.SetOutput(buf)
+
+			req := httptest.NewRequest(http.MethodGet, "/", nil)
+			rec := httptest.NewRecorder()
+			c := e.NewContext(req, rec)
+
+			config := DefaultRecoverConfig
+			config.LogLevel = tt.logLevel
+			h := RecoverWithConfig(config)(echo.HandlerFunc(func(c echo.Context) error {
+				panic("test")
+			}))
+
+			h(c)
+
+			assert.Equal(t, http.StatusInternalServerError, rec.Code)
+
+			output := buf.String()
+			if tt.logLevel == log.OFF {
+				assert.Empty(t, output)
+			} else {
+				assert.Contains(t, output, "PANIC RECOVER")
+				assert.Contains(t, output, fmt.Sprintf(`"level":"%s"`, tt.levelName))
+			}
+		})
+	}
+}