feat: add database layer with SQLite and PostgreSQL support

Database Package (internal/database/):
- Database interface abstraction for multiple backends
- SQLite implementation with pure Go driver (no CGO)
- PostgreSQL implementation with connection pooling
- Factory pattern for creating database from config
- Tiered retention with automatic aggregation:
  - Raw metrics: 24h (5s resolution)
  - 1-minute aggregation: 7 days
  - 5-minute aggregation: 30 days
  - Hourly aggregation: 1 year

Schema includes:
- agents: registration, status, certificates
- users: local + LDAP authentication
- roles: RBAC with permissions JSON
- sessions: token-based authentication
- metrics_*: time-series with aggregation
- alerts: triggered alerts with acknowledgment

Configuration Updates:
- DatabaseConfig with SQLite path and PostgreSQL settings
- RetentionConfig for customizing data retention
- Environment variables: TYTO_DB_*, TYTO_DB_CONNECTION_STRING
- Default SQLite at /var/lib/tyto/tyto.db

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-28 08:18:48 +01:00
parent 50a7a774ea
commit c0e678931d
8 changed files with 2588 additions and 14 deletions

View File

@@ -75,8 +75,8 @@ func runServer(cfg *config.Config) {
// Initialize agent registry
registryPath := "/var/lib/tyto/agents.json"
if cfg.Database.SQLitePath != "" {
registryPath = cfg.Database.SQLitePath + ".agents.json"
if cfg.Database.Path != "" {
registryPath = cfg.Database.Path + ".agents.json"
}
registry := server.NewRegistry(registryPath)
log.Printf("Agent registry initialized: %s", registryPath)

View File

@@ -8,9 +8,11 @@ require github.com/gin-contrib/cors v1.7.2
require (
github.com/godbus/dbus/v5 v5.1.0
github.com/lib/pq v1.10.9
google.golang.org/grpc v1.68.0
google.golang.org/protobuf v1.35.2
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.34.4
)
require (
@@ -18,12 +20,15 @@ require (
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kr/text v0.2.0 // indirect
@@ -31,7 +36,9 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
@@ -40,4 +47,10 @@ require (
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)

View File

@@ -10,6 +10,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
@@ -35,6 +37,12 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@@ -47,6 +55,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -54,10 +64,14 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
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/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.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -81,14 +95,20 @@ golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
@@ -101,5 +121,31 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -115,15 +115,27 @@ type DatabaseConfig struct {
Type string `yaml:"type"`
// SQLite settings
SQLitePath string `yaml:"sqlite_path"`
Path string `yaml:"path"` // Path to SQLite database file
// PostgreSQL settings
PostgresHost string `yaml:"postgres_host"`
PostgresPort int `yaml:"postgres_port"`
PostgresUser string `yaml:"postgres_user"`
PostgresPassword string `yaml:"postgres_password"`
PostgresDatabase string `yaml:"postgres_database"`
PostgresSSLMode string `yaml:"postgres_sslmode"`
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
Password string `yaml:"password"`
Database string `yaml:"database"`
SSLMode string `yaml:"sslmode"`
ConnectionString string `yaml:"connection_string"` // Full connection string (overrides individual fields)
// Retention settings
Retention RetentionConfig `yaml:"retention"`
}
// RetentionConfig defines data retention policies.
type RetentionConfig struct {
RawRetention time.Duration `yaml:"raw"` // Raw metrics retention (default: 24h)
OneMinuteRetention time.Duration `yaml:"one_minute"` // 1-minute aggregation (default: 7d)
FiveMinuteRetention time.Duration `yaml:"five_minute"` // 5-minute aggregation (default: 30d)
HourlyRetention time.Duration `yaml:"hourly"` // Hourly aggregation (default: 1y)
}
type AlertConfig struct {
@@ -160,8 +172,10 @@ func Load() *Config {
IntervalSeconds: 5,
},
Database: DatabaseConfig{
Type: "sqlite",
SQLitePath: "/var/lib/tyto/tyto.db",
Type: "sqlite",
Path: "/var/lib/tyto/tyto.db",
Port: 5432,
SSLMode: "disable",
},
}
@@ -214,7 +228,22 @@ func Load() *Config {
cfg.Database.Type = val
}
if val := os.Getenv("TYTO_DB_PATH"); val != "" {
cfg.Database.SQLitePath = val
cfg.Database.Path = val
}
if val := os.Getenv("TYTO_DB_HOST"); val != "" {
cfg.Database.Host = val
}
if val := os.Getenv("TYTO_DB_USER"); val != "" {
cfg.Database.User = val
}
if val := os.Getenv("TYTO_DB_PASSWORD"); val != "" {
cfg.Database.Password = val
}
if val := os.Getenv("TYTO_DB_NAME"); val != "" {
cfg.Database.Database = val
}
if val := os.Getenv("TYTO_DB_CONNECTION_STRING"); val != "" {
cfg.Database.ConnectionString = val
}
// Agent configuration
@@ -269,8 +298,10 @@ func DefaultConfig() *Config {
Interval: 5 * time.Second,
},
Database: DatabaseConfig{
Type: "sqlite",
SQLitePath: "/var/lib/tyto/tyto.db",
Type: "sqlite",
Path: "/var/lib/tyto/tyto.db",
Port: 5432,
SSLMode: "disable",
},
RefreshInterval: 5 * time.Second,
}

View File

@@ -0,0 +1,208 @@
// Package database provides database abstraction for Tyto.
// Supports both SQLite (default) and PostgreSQL backends.
package database
import (
"context"
"time"
"tyto/internal/models"
)
// Database defines the interface for all database operations.
type Database interface {
// Lifecycle
Close() error
Migrate() error
// Metrics storage
StoreMetrics(ctx context.Context, agentID string, metrics *models.AllMetrics) error
QueryMetrics(ctx context.Context, agentID string, from, to time.Time, resolution string) ([]MetricPoint, error)
GetLatestMetrics(ctx context.Context, agentID string) (*models.AllMetrics, error)
// Agents (extends registry with persistence)
StoreAgent(ctx context.Context, agent *Agent) error
GetAgent(ctx context.Context, id string) (*Agent, error)
ListAgents(ctx context.Context) ([]*Agent, error)
UpdateAgentStatus(ctx context.Context, id string, status AgentStatus, lastSeen time.Time) error
DeleteAgent(ctx context.Context, id string) error
// Users
CreateUser(ctx context.Context, user *User) error
GetUser(ctx context.Context, id string) (*User, error)
GetUserByUsername(ctx context.Context, username string) (*User, error)
UpdateUser(ctx context.Context, user *User) error
DeleteUser(ctx context.Context, id string) error
ListUsers(ctx context.Context) ([]*User, error)
// Roles
CreateRole(ctx context.Context, role *Role) error
GetRole(ctx context.Context, id string) (*Role, error)
ListRoles(ctx context.Context) ([]*Role, error)
UpdateRole(ctx context.Context, role *Role) error
DeleteRole(ctx context.Context, id string) error
GetUserRoles(ctx context.Context, userID string) ([]*Role, error)
AssignRole(ctx context.Context, userID, roleID string) error
RemoveRole(ctx context.Context, userID, roleID string) error
// Sessions
CreateSession(ctx context.Context, session *Session) error
GetSession(ctx context.Context, token string) (*Session, error)
DeleteSession(ctx context.Context, token string) error
DeleteUserSessions(ctx context.Context, userID string) error
CleanupExpiredSessions(ctx context.Context) error
// Alerts
StoreAlert(ctx context.Context, alert *Alert) error
GetAlert(ctx context.Context, id string) (*Alert, error)
QueryAlerts(ctx context.Context, filter AlertFilter) ([]*Alert, error)
AcknowledgeAlert(ctx context.Context, id string) error
// Retention
RunRetention(ctx context.Context) error
}
// MetricPoint represents a single metric data point.
type MetricPoint struct {
Timestamp time.Time `json:"timestamp"`
AgentID string `json:"agentId"`
// Aggregated values
CPUAvg float64 `json:"cpuAvg"`
CPUMin float64 `json:"cpuMin"`
CPUMax float64 `json:"cpuMax"`
MemoryAvg float64 `json:"memoryAvg"`
MemoryMin float64 `json:"memoryMin"`
MemoryMax float64 `json:"memoryMax"`
DiskAvg float64 `json:"diskAvg,omitempty"`
GPUAvg float64 `json:"gpuAvg,omitempty"`
}
// AgentStatus represents agent connection state.
type AgentStatus string
const (
AgentStatusPending AgentStatus = "pending"
AgentStatusApproved AgentStatus = "approved"
AgentStatusConnected AgentStatus = "connected"
AgentStatusOffline AgentStatus = "offline"
AgentStatusRevoked AgentStatus = "revoked"
)
// Agent represents a registered monitoring agent.
type Agent struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Hostname string `json:"hostname"`
OS string `json:"os"`
Architecture string `json:"architecture"`
Version string `json:"version"`
Capabilities []string `json:"capabilities,omitempty"`
Status AgentStatus `json:"status"`
CertSerial string `json:"certSerial,omitempty"`
CertExpiry time.Time `json:"certExpiry,omitempty"`
LastSeen time.Time `json:"lastSeen,omitempty"`
RegisteredAt time.Time `json:"registeredAt"`
Tags []string `json:"tags,omitempty"`
}
// AuthProvider indicates how a user authenticates.
type AuthProvider string
const (
AuthProviderLocal AuthProvider = "local"
AuthProviderLDAP AuthProvider = "ldap"
)
// User represents a system user.
type User struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email,omitempty"`
PasswordHash []byte `json:"-"`
AuthProvider AuthProvider `json:"authProvider"`
LDAPDN string `json:"ldapDn,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
LastLogin time.Time `json:"lastLogin,omitempty"`
Disabled bool `json:"disabled"`
}
// Role represents a set of permissions.
type Role struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Permissions []string `json:"permissions"`
IsSystem bool `json:"isSystem"`
CreatedAt time.Time `json:"createdAt"`
}
// Session represents an authenticated user session.
type Session struct {
Token string `json:"token"`
UserID string `json:"userId"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
IPAddress string `json:"ipAddress,omitempty"`
UserAgent string `json:"userAgent,omitempty"`
}
// AlertSeverity indicates alert severity level.
type AlertSeverity string
const (
AlertSeverityWarning AlertSeverity = "warning"
AlertSeverityCritical AlertSeverity = "critical"
)
// Alert represents a triggered alert.
type Alert struct {
ID string `json:"id"`
AgentID string `json:"agentId"`
Type string `json:"type"`
Severity AlertSeverity `json:"severity"`
Message string `json:"message"`
Value float64 `json:"value"`
Threshold float64 `json:"threshold"`
TriggeredAt time.Time `json:"triggeredAt"`
ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
Acknowledged bool `json:"acknowledged"`
}
// AlertFilter specifies criteria for querying alerts.
type AlertFilter struct {
AgentID string
Type string
Severity AlertSeverity
Acknowledged *bool
From time.Time
To time.Time
Limit int
Offset int
}
// RetentionConfig defines data retention policies.
type RetentionConfig struct {
// Raw metrics retention (default: 24 hours)
RawRetention time.Duration
// 1-minute aggregation retention (default: 7 days)
OneMinuteRetention time.Duration
// 5-minute aggregation retention (default: 30 days)
FiveMinuteRetention time.Duration
// Hourly aggregation retention (default: 1 year)
HourlyRetention time.Duration
}
// DefaultRetentionConfig returns default retention settings.
func DefaultRetentionConfig() RetentionConfig {
return RetentionConfig{
RawRetention: 24 * time.Hour,
OneMinuteRetention: 7 * 24 * time.Hour,
FiveMinuteRetention: 30 * 24 * time.Hour,
HourlyRetention: 365 * 24 * time.Hour,
}
}

View File

@@ -0,0 +1,90 @@
// Package database provides database factory for creating the appropriate backend.
package database
import (
"fmt"
"os"
"path/filepath"
"tyto/internal/config"
)
// New creates a new database connection based on configuration.
func New(cfg *config.DatabaseConfig) (Database, error) {
retention := DefaultRetentionConfig()
// Override retention from config if provided
if cfg.Retention.RawRetention > 0 {
retention.RawRetention = cfg.Retention.RawRetention
}
if cfg.Retention.OneMinuteRetention > 0 {
retention.OneMinuteRetention = cfg.Retention.OneMinuteRetention
}
if cfg.Retention.FiveMinuteRetention > 0 {
retention.FiveMinuteRetention = cfg.Retention.FiveMinuteRetention
}
if cfg.Retention.HourlyRetention > 0 {
retention.HourlyRetention = cfg.Retention.HourlyRetention
}
switch cfg.Type {
case "sqlite", "":
return newSQLiteFromConfig(cfg, retention)
case "postgres", "postgresql":
return newPostgresFromConfig(cfg, retention)
default:
return nil, fmt.Errorf("unsupported database type: %s", cfg.Type)
}
}
func newSQLiteFromConfig(cfg *config.DatabaseConfig, retention RetentionConfig) (*SQLiteDB, error) {
path := cfg.Path
if path == "" {
path = "tyto.db"
}
// Ensure directory exists
dir := filepath.Dir(path)
if dir != "" && dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("create database directory: %w", err)
}
}
db, err := NewSQLiteDB(path, retention)
if err != nil {
return nil, err
}
// Run migrations
if err := db.Migrate(); err != nil {
db.Close()
return nil, fmt.Errorf("migrate: %w", err)
}
return db, nil
}
func newPostgresFromConfig(cfg *config.DatabaseConfig, retention RetentionConfig) (*PostgresDB, error) {
connStr := cfg.ConnectionString
if connStr == "" {
// Build connection string from individual fields
connStr = fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.Database, cfg.SSLMode,
)
}
db, err := NewPostgresDB(connStr, retention)
if err != nil {
return nil, err
}
// Run migrations
if err := db.Migrate(); err != nil {
db.Close()
return nil, fmt.Errorf("migrate: %w", err)
}
return db, nil
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff