Repository navigation
Expand file tree
/
Copy pathlogsql.go
More file actions
138 lines (116 loc) · 3.2 KB
/
Copy pathlogsql.go
File metadata and controls
138 lines (116 loc) · 3.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package logsql
import (
"context"
"fmt"
"log/slog"
"slices"
"strings"
"time"
"github.com/coredns/coredns/plugin"
"github.com/jmoiron/sqlx"
"github.com/miekg/dns"
)
const (
// DomainChannelBufferSize is the size of the channel buffer for domains.
DomainChannelBufferSize = 1024
)
// LogSql is a plugin that logs DNS queries to a SQL database.
type LogSql struct {
Next plugin.Handler // Next plugin in the chain
DB *sqlx.DB // Database connection
domainsCh chan []string // Channel for sending domains to background goroutine
doneCh chan struct{} // Channel to signal background goroutine to stop
}
// Request represents a DNS request record in the database.
type Request struct {
Domain string `db:"domain"` // Domain name
CreatedAt time.Time `db:"created_at"` // Timestamp of creation
UpdatedAt time.Time `db:"updated_at"` // Timestamp of last update
}
// NewLogSql creates a new LogSql instance.
func NewLogSql(next plugin.Handler, db *sqlx.DB) *LogSql {
// Initialize LogSql
ls := &LogSql{
Next: next,
DB: db,
domainsCh: make(chan []string, DomainChannelBufferSize),
doneCh: make(chan struct{}),
}
// Spin up background goroutine to handle database inserts
go func() {
defer close(ls.doneCh)
for domains := range ls.domainsCh {
err := ls.insertIntoDB(domains)
if err != nil {
slog.Error("[logsql] Failed to insert domains into database", slog.Any("error", err))
}
}
}()
return ls
}
// Name implements the plugin Handler interface.
func (ls LogSql) Name() string {
return "logsql"
}
// Close gracefully shuts down the plugin.
func (ls *LogSql) Close() {
// Signal background goroutine to stop and wait for it to finish
close(ls.domainsCh)
<-ls.doneCh
}
// ServeDNS implements the plugin.Handler interface.
func (ls LogSql) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
// Call next plugin
rw := NewResponseWriter(w)
val, err := plugin.NextOrFailure(ls.Name(), ls.Next, ctx, rw, r)
// Send domains to background goroutine for database insertion
ls.domainsCh <- rw.Domains()
return val, err
}
// insertIntoDB inserts the given domains into the database.
func (ls LogSql) insertIntoDB(domains []string) error {
// Early return if there are no domains
if len(domains) == 0 {
return nil
}
// Prepare requests
requests := make([]Request, 0, len(domains))
for _, d := range domains {
requests = append(requests, Request{
Domain: d,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
}
// Prevent deadlocks by forcing a stable order
slices.SortStableFunc(requests, func(l Request, r Request) int {
return strings.Compare(l.Domain, r.Domain)
})
// Insert record into database
query, args, err := sqlx.Named(
`
INSERT INTO "answers" (
"domain",
"created_at",
"updated_at"
)
VALUES (
:domain,
:created_at,
:updated_at
)
ON CONFLICT ("domain") DO UPDATE
SET
"updated_at" = EXCLUDED."updated_at"
`,
requests,
)
if err != nil {
return fmt.Errorf("failed to prepare named query: %w", err)
}
_, err = ls.DB.Exec(ls.DB.Rebind(query), args...)
if err != nil {
return fmt.Errorf("failed to execute named query: %w", err)
}
return nil
}