diff --git a/CHANGELOG.md b/CHANGELOG.md index b5477285..4e7c40af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,9 @@ - Added `Cross-Origin-Opener-Policy:same-origin` to the default security response headers. _This is an extra precaution to prevent tab-nabbing in case custom UI plugins use `target="_blank"` without `rel="noopener"`._ -- Added new log settings option to limit the max `Log.Data` size (default to ~16KB). +- Added new log settings option to limit the max `Log.Data` size that will be saved in the database (default to ~16KB). _This is an extra precaution for the cases when logging user supplied data without validating it beforehand._ _If the resulting `Log.Data` json is above the limit, it is truncated to the last valid decoded character and an extra `"__pb_truncated__":true` log data entry will be added.`_ + _Additionally, for just in case the log message is also truncated at max 8k characters._ - (@todo) Bumped the min Go version to 1.27.0 and migrated to the new `encoding/json/v2` package. diff --git a/core/log_model.go b/core/log_model.go index 7fef8da0..8fa29028 100644 --- a/core/log_model.go +++ b/core/log_model.go @@ -13,7 +13,10 @@ var ( const LogsTableName = "_logs" -const defaultMaxLogDataSize = 16 << 10 // ~16kb +const ( + defaultMaxLogDataSize = 16 << 10 // ~16kb + defaultMaxLogMessageSize = 8000 +) type Log struct { BaseModel @@ -39,15 +42,10 @@ func (l *Log) DBExport(app App) (map[string]any, error) { "level": l.Level, } - maxDataSize := app.Settings().Logs.MaxDataSize - if maxDataSize == 0 { - maxDataSize = defaultMaxLogDataSize - } - // truncate the raw message bytes // (this is expected to be very rare so it is ok even if multi-byte chars) - if int64(len(l.Message)) > maxDataSize { - result["message"] = l.Message[:maxDataSize] + if int64(len(l.Message)) > defaultMaxLogMessageSize { + result["message"] = l.Message[:defaultMaxLogMessageSize] } else { result["message"] = l.Message } @@ -55,6 +53,11 @@ func (l *Log) DBExport(app App) (map[string]any, error) { if len(l.Data) == 0 { result["data"] = l.Data } else { + maxDataSize := app.Settings().Logs.MaxDataSize + if maxDataSize == 0 { + maxDataSize = defaultMaxLogDataSize + } + rawData, err := l.Data.MarshalJSON() if int64(len(rawData)) > maxDataSize { truncatedData := types.JSONMap[any]{} diff --git a/core/log_model_test.go b/core/log_model_test.go index ab3bfc92..2e3c170f 100644 --- a/core/log_model_test.go +++ b/core/log_model_test.go @@ -32,7 +32,8 @@ func TestLogDBExport(t *testing.T) { t.Fatal(err) } - defaultLimit := 16 << 10 + messageLimit := 8000 + dataLimit := 16 << 10 scenarios := []struct { name string @@ -47,7 +48,7 @@ func TestLogDBExport(t *testing.T) { `{"created":"","data":{},"id":"","level":0,"message":""}`, }, { - "with message and data below the default limit", + "with message and data below the default limits", core.Log{ BaseModel: core.BaseModel{Id: "test_id"}, Created: date, @@ -59,40 +60,40 @@ func TestLogDBExport(t *testing.T) { `{"created":"2026-08-18 10:20:30.456Z","data":{"a":"test1","b":"test2"},"id":"test_id","level":123,"message":"test_message"}`, }, { - "with message and data exactly the default limit", + "with message and data exactly the default limits", core.Log{ BaseModel: core.BaseModel{Id: "test_id"}, Created: date, Level: 123, - Message: strings.Repeat("a", defaultLimit), - Data: types.JSONMap[any]{"a": "test1", "b": "test2", "c": strings.Repeat("a", defaultLimit-32)}, + Message: strings.Repeat("a", messageLimit), + Data: types.JSONMap[any]{"a": "test1", "b": "test2", "c": strings.Repeat("a", dataLimit-32)}, }, 0, - `{"created":"2026-08-18 10:20:30.456Z","data":{"a":"test1","b":"test2","c":"` + strings.Repeat("a", defaultLimit-32) + `"},"id":"test_id","level":123,"message":"` + strings.Repeat("a", defaultLimit) + `"}`, + `{"created":"2026-08-18 10:20:30.456Z","data":{"a":"test1","b":"test2","c":"` + strings.Repeat("a", dataLimit-32) + `"},"id":"test_id","level":123,"message":"` + strings.Repeat("a", messageLimit) + `"}`, }, { - "with message and data above the default limit", + "with message and data above the default limits", core.Log{ BaseModel: core.BaseModel{Id: "test_id"}, Created: date, Level: 123, - Message: strings.Repeat("a", defaultLimit) + "x", // "x" should be omitted - Data: types.JSONMap[any]{"a": "test1", "b": "test2", "c": strings.Repeat("a", defaultLimit-32) + "x"}, // the end will be incomplete and something like `"c":"...aaaaaax` + Message: strings.Repeat("a", messageLimit) + "x", // "x" should be omitted + Data: types.JSONMap[any]{"a": "test1", "b": "test2", "c": strings.Repeat("a", dataLimit-32) + "x"}, // the end will be incomplete and something like `"c":"...aaaaaax` }, 0, - `{"created":"2026-08-18 10:20:30.456Z","data":{"__pb_truncated__":true,"a":"test1","b":"test2","c":"` + strings.Repeat("a", defaultLimit-32) + `x"},"id":"test_id","level":123,"message":"` + strings.Repeat("a", defaultLimit) + `"}`, + `{"created":"2026-08-18 10:20:30.456Z","data":{"__pb_truncated__":true,"a":"test1","b":"test2","c":"` + strings.Repeat("a", dataLimit-32) + `x"},"id":"test_id","level":123,"message":"` + strings.Repeat("a", messageLimit) + `"}`, }, { - "with message and data above custom limit", + "with data above custom limit", core.Log{ BaseModel: core.BaseModel{Id: "test_id"}, Created: date, Level: 123, - Message: strings.Repeat("a", 2<<10) + "x", // "x" should be omitted + Message: "test_message", Data: types.JSONMap[any]{"a": "test1", "b": "test2", "c": strings.Repeat("a", (2<<10)-32) + "x"}, // the end will be incomplete and something like `"c":"...aaaaaax` }, 2 << 10, - `{"created":"2026-08-18 10:20:30.456Z","data":{"__pb_truncated__":true,"a":"test1","b":"test2","c":"` + strings.Repeat("a", (2<<10)-32) + `x"},"id":"test_id","level":123,"message":"` + strings.Repeat("a", 2<<10) + `"}`, + `{"created":"2026-08-18 10:20:30.456Z","data":{"__pb_truncated__":true,"a":"test1","b":"test2","c":"` + strings.Repeat("a", (2<<10)-32) + `x"},"id":"test_id","level":123,"message":"test_message"}`, }, }