Compare commits

...

1 Commits

Author SHA1 Message Date
Andres Torres d2836657c4 refactor(setting): Allow public use of reading typed quota & dataproxy settings 2025-11-20 14:03:42 -05:00
4 changed files with 314 additions and 41 deletions
+60 -21
View File
@@ -8,29 +8,68 @@ import (
const defaultDataProxyRowLimit = int64(1000000)
type ProxySettings struct {
SendUserHeader bool
Logging bool
Timeout int
DialTimeout int
KeepAlive int
TLSHandshakeTimeout int
ExpectContinueTimeout int
MaxConnsPerHost int
MaxIdleConns int
IdleConnTimeout int
ResponseLimit int64
RowLimit int64
UserAgent string
}
func readDataProxySettings(iniFile *ini.File, cfg *Cfg) error {
dataproxy := iniFile.Section("dataproxy")
cfg.SendUserHeader = dataproxy.Key("send_user_header").MustBool(false)
cfg.DataProxyLogging = dataproxy.Key("logging").MustBool(false)
cfg.DataProxyTimeout = dataproxy.Key("timeout").MustInt(30)
cfg.DataProxyDialTimeout = dataproxy.Key("dialTimeout").MustInt(10)
cfg.DataProxyKeepAlive = dataproxy.Key("keep_alive_seconds").MustInt(30)
cfg.DataProxyTLSHandshakeTimeout = dataproxy.Key("tls_handshake_timeout_seconds").MustInt(10)
cfg.DataProxyExpectContinueTimeout = dataproxy.Key("expect_continue_timeout_seconds").MustInt(1)
cfg.DataProxyMaxConnsPerHost = dataproxy.Key("max_conns_per_host").MustInt(0)
cfg.DataProxyMaxIdleConns = dataproxy.Key("max_idle_connections").MustInt()
cfg.DataProxyIdleConnTimeout = dataproxy.Key("idle_conn_timeout_seconds").MustInt(90)
cfg.ResponseLimit = dataproxy.Key("response_limit").MustInt64(0)
cfg.DataProxyRowLimit = dataproxy.Key("row_limit").MustInt64(defaultDataProxyRowLimit)
cfg.DataProxyUserAgent = dataproxy.Key("user_agent").String()
proxy := ReadDataProxySettings(iniFile)
if cfg.DataProxyUserAgent == "" {
cfg.DataProxyUserAgent = fmt.Sprintf("Grafana/%s", BuildVersion)
}
if cfg.DataProxyRowLimit <= 0 {
cfg.DataProxyRowLimit = defaultDataProxyRowLimit
}
cfg.SendUserHeader = proxy.SendUserHeader
cfg.DataProxyLogging = proxy.Logging
cfg.DataProxyTimeout = proxy.Timeout
cfg.DataProxyDialTimeout = proxy.DialTimeout
cfg.DataProxyKeepAlive = proxy.KeepAlive
cfg.DataProxyTLSHandshakeTimeout = proxy.TLSHandshakeTimeout
cfg.DataProxyExpectContinueTimeout = proxy.ExpectContinueTimeout
cfg.DataProxyMaxConnsPerHost = proxy.MaxConnsPerHost
cfg.DataProxyMaxIdleConns = proxy.MaxIdleConns
cfg.DataProxyIdleConnTimeout = proxy.IdleConnTimeout
cfg.ResponseLimit = proxy.ResponseLimit
cfg.DataProxyRowLimit = proxy.RowLimit
cfg.DataProxyUserAgent = proxy.UserAgent
return nil
}
func ReadDataProxySettings(iniFile *ini.File) ProxySettings {
section := iniFile.Section("dataproxy")
proxy := ProxySettings{
SendUserHeader: section.Key("send_user_header").MustBool(false),
Logging: section.Key("logging").MustBool(false),
Timeout: section.Key("timeout").MustInt(30),
DialTimeout: section.Key("dialTimeout").MustInt(10),
KeepAlive: section.Key("keep_alive_seconds").MustInt(30),
TLSHandshakeTimeout: section.Key("tls_handshake_timeout_seconds").MustInt(10),
ExpectContinueTimeout: section.Key("expect_continue_timeout_seconds").MustInt(1),
MaxConnsPerHost: section.Key("max_conns_per_host").MustInt(0),
MaxIdleConns: section.Key("max_idle_connections").MustInt(),
IdleConnTimeout: section.Key("idle_conn_timeout_seconds").MustInt(90),
ResponseLimit: section.Key("response_limit").MustInt64(0),
RowLimit: section.Key("row_limit").MustInt64(defaultDataProxyRowLimit),
UserAgent: section.Key("user_agent").String(),
}
if proxy.UserAgent == "" {
proxy.UserAgent = fmt.Sprintf("Grafana/%s", BuildVersion)
}
if proxy.RowLimit <= 0 {
proxy.RowLimit = defaultDataProxyRowLimit
}
return proxy
}
+112
View File
@@ -0,0 +1,112 @@
package setting
import (
"regexp"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/ini.v1"
)
func TestReadDataProxySettings(t *testing.T) {
t.Run("should use default values when ini is empty", func(t *testing.T) {
f := ini.Empty()
proxy := ReadDataProxySettings(f)
assertDataProxyDefaults(t, proxy)
})
t.Run("should use default values when section exists with no values", func(t *testing.T) {
f := ini.Empty()
_, err := f.NewSection("dataproxy")
require.NoError(t, err)
proxy := ReadDataProxySettings(f)
assertDataProxyDefaults(t, proxy)
})
t.Run("should use custom values when section has overrides", func(t *testing.T) {
iniFile := `
[dataproxy]
send_user_header = true
logging = true
timeout = 60
dialTimeout = 20
keep_alive_seconds = 60
tls_handshake_timeout_seconds = 20
expect_continue_timeout_seconds = 2
max_conns_per_host = 100
max_idle_connections = 50
idle_conn_timeout_seconds = 120
response_limit = 5242880
row_limit = 500000
user_agent = CustomAgent/1.0
`
f, err := ini.Load([]byte(iniFile))
require.NoError(t, err)
proxy := ReadDataProxySettings(f)
assert.True(t, proxy.SendUserHeader)
assert.True(t, proxy.Logging)
assert.Equal(t, 60, proxy.Timeout)
assert.Equal(t, 20, proxy.DialTimeout)
assert.Equal(t, 60, proxy.KeepAlive)
assert.Equal(t, 20, proxy.TLSHandshakeTimeout)
assert.Equal(t, 2, proxy.ExpectContinueTimeout)
assert.Equal(t, 100, proxy.MaxConnsPerHost)
assert.Equal(t, 50, proxy.MaxIdleConns)
assert.Equal(t, 120, proxy.IdleConnTimeout)
assert.Equal(t, int64(5242880), proxy.ResponseLimit)
assert.Equal(t, int64(500000), proxy.RowLimit)
assert.Equal(t, "CustomAgent/1.0", proxy.UserAgent)
})
t.Run("should set default row limit when row_limit is zero", func(t *testing.T) {
iniFile := `
[dataproxy]
row_limit = 0
`
f, err := ini.Load([]byte(iniFile))
require.NoError(t, err)
proxy := ReadDataProxySettings(f)
assert.Equal(t, defaultDataProxyRowLimit, proxy.RowLimit)
})
t.Run("should set default row limit when row_limit is negative", func(t *testing.T) {
iniFile := `
[dataproxy]
row_limit = -100
`
f, err := ini.Load([]byte(iniFile))
require.NoError(t, err)
proxy := ReadDataProxySettings(f)
assert.Equal(t, defaultDataProxyRowLimit, proxy.RowLimit)
})
}
func assertDataProxyDefaults(t *testing.T, proxy ProxySettings) {
t.Helper()
assert.False(t, proxy.SendUserHeader)
assert.False(t, proxy.Logging)
assert.Equal(t, 30, proxy.Timeout)
assert.Equal(t, 10, proxy.DialTimeout)
assert.Equal(t, 30, proxy.KeepAlive)
assert.Equal(t, 10, proxy.TLSHandshakeTimeout)
assert.Equal(t, 1, proxy.ExpectContinueTimeout)
assert.Equal(t, 0, proxy.MaxConnsPerHost)
assert.Equal(t, 0, proxy.MaxIdleConns)
assert.Equal(t, 90, proxy.IdleConnTimeout)
assert.Equal(t, int64(0), proxy.ResponseLimit)
assert.Equal(t, defaultDataProxyRowLimit, proxy.RowLimit)
// UserAgent should match "Grafana/{version}" pattern
assert.Regexp(t, regexp.MustCompile(`^Grafana/.*`), proxy.UserAgent)
}
+30 -20
View File
@@ -1,5 +1,7 @@
package setting
import "gopkg.in/ini.v1"
type OrgQuota struct {
User int64 `target:"org_user"`
DataSource int64 `target:"data_source"`
@@ -32,34 +34,42 @@ type QuotaSettings struct {
}
func (cfg *Cfg) readQuotaSettings() {
iniFile := cfg.Raw
quota := ReadQuotaSettings(iniFile)
// set global defaults.
quota := cfg.Raw.Section("quota")
cfg.Quota.Enabled = quota.Key("enabled").MustBool(false)
cfg.Quota = quota
}
func ReadQuotaSettings(iniFile *ini.File) QuotaSettings {
section := iniFile.Section("quota")
var quota QuotaSettings
quota.Enabled = section.Key("enabled").MustBool(false)
// per ORG Limits
cfg.Quota.Org = OrgQuota{
User: quota.Key("org_user").MustInt64(10),
DataSource: quota.Key("org_data_source").MustInt64(10),
Dashboard: quota.Key("org_dashboard").MustInt64(10),
ApiKey: quota.Key("org_api_key").MustInt64(10),
AlertRule: quota.Key("org_alert_rule").MustInt64(100),
quota.Org = OrgQuota{
User: section.Key("org_user").MustInt64(10),
DataSource: section.Key("org_data_source").MustInt64(10),
Dashboard: section.Key("org_dashboard").MustInt64(10),
ApiKey: section.Key("org_api_key").MustInt64(10),
AlertRule: section.Key("org_alert_rule").MustInt64(100),
}
// per User limits
cfg.Quota.User = UserQuota{
Org: quota.Key("user_org").MustInt64(10),
quota.User = UserQuota{
Org: section.Key("user_org").MustInt64(10),
}
// Global Limits
cfg.Quota.Global = GlobalQuota{
User: quota.Key("global_user").MustInt64(-1),
Org: quota.Key("global_org").MustInt64(-1),
DataSource: quota.Key("global_data_source").MustInt64(-1),
Dashboard: quota.Key("global_dashboard").MustInt64(-1),
ApiKey: quota.Key("global_api_key").MustInt64(-1),
Session: quota.Key("global_session").MustInt64(-1),
File: quota.Key("global_file").MustInt64(-1),
AlertRule: quota.Key("global_alert_rule").MustInt64(-1),
Correlations: quota.Key("global_correlations").MustInt64(-1),
quota.Global = GlobalQuota{
User: section.Key("global_user").MustInt64(-1),
Org: section.Key("global_org").MustInt64(-1),
DataSource: section.Key("global_data_source").MustInt64(-1),
Dashboard: section.Key("global_dashboard").MustInt64(-1),
ApiKey: section.Key("global_api_key").MustInt64(-1),
Session: section.Key("global_session").MustInt64(-1),
File: section.Key("global_file").MustInt64(-1),
AlertRule: section.Key("global_alert_rule").MustInt64(-1),
Correlations: section.Key("global_correlations").MustInt64(-1),
}
return quota
}
+112
View File
@@ -0,0 +1,112 @@
package setting
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/ini.v1"
)
func TestReadQuotaSettings(t *testing.T) {
t.Run("should use custom values when section has overrides", func(t *testing.T) {
iniFile := `
[quota]
enabled = true
# Org quotas
org_user = 20
org_data_source = 30
org_dashboard = 40
org_api_key = 50
org_alert_rule = 200
# User quotas
user_org = 5
# Global quotas
global_user = 1000
global_org = 500
global_data_source = 2000
global_dashboard = 3000
global_api_key = 100
global_session = 10000
global_file = 500
global_alert_rule = 5000
global_correlations = 250
`
f, err := ini.Load([]byte(iniFile))
require.NoError(t, err)
quota := ReadQuotaSettings(f)
// Enabled should be true
assert.True(t, quota.Enabled)
// Org quotas should have custom values
assert.Equal(t, int64(20), quota.Org.User)
assert.Equal(t, int64(30), quota.Org.DataSource)
assert.Equal(t, int64(40), quota.Org.Dashboard)
assert.Equal(t, int64(50), quota.Org.ApiKey)
assert.Equal(t, int64(200), quota.Org.AlertRule)
// User quotas should have custom values
assert.Equal(t, int64(5), quota.User.Org)
// Global quotas should have custom values
assert.Equal(t, int64(1000), quota.Global.User)
assert.Equal(t, int64(500), quota.Global.Org)
assert.Equal(t, int64(2000), quota.Global.DataSource)
assert.Equal(t, int64(3000), quota.Global.Dashboard)
assert.Equal(t, int64(100), quota.Global.ApiKey)
assert.Equal(t, int64(10000), quota.Global.Session)
assert.Equal(t, int64(500), quota.Global.File)
assert.Equal(t, int64(5000), quota.Global.AlertRule)
assert.Equal(t, int64(250), quota.Global.Correlations)
})
t.Run("should use default values when ini is empty", func(t *testing.T) {
f := ini.Empty()
quota := ReadQuotaSettings(f)
assertDefaults(t, quota)
})
t.Run("should use default values when section exists with no values", func(t *testing.T) {
f := ini.Empty()
_, err := f.NewSection("quota")
require.NoError(t, err)
quota := ReadQuotaSettings(f)
assertDefaults(t, quota)
})
}
func assertDefaults(t *testing.T, quota QuotaSettings) {
t.Helper()
// Enabled should be false by default
assert.False(t, quota.Enabled)
// Org quotas should have default values
assert.Equal(t, int64(10), quota.Org.User)
assert.Equal(t, int64(10), quota.Org.DataSource)
assert.Equal(t, int64(10), quota.Org.Dashboard)
assert.Equal(t, int64(10), quota.Org.ApiKey)
assert.Equal(t, int64(100), quota.Org.AlertRule)
// User quotas should have default values
assert.Equal(t, int64(10), quota.User.Org)
// Global quotas should have default values (-1 means unlimited)
assert.Equal(t, int64(-1), quota.Global.User)
assert.Equal(t, int64(-1), quota.Global.Org)
assert.Equal(t, int64(-1), quota.Global.DataSource)
assert.Equal(t, int64(-1), quota.Global.Dashboard)
assert.Equal(t, int64(-1), quota.Global.ApiKey)
assert.Equal(t, int64(-1), quota.Global.Session)
assert.Equal(t, int64(-1), quota.Global.File)
assert.Equal(t, int64(-1), quota.Global.AlertRule)
assert.Equal(t, int64(-1), quota.Global.Correlations)
}