-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
332 lines (280 loc) · 9.52 KB
/
main.go
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
package main
import (
"database/sql"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
_ "github.com/mattn/go-sqlite3"
"gopkg.in/yaml.v2"
)
// Structures
type Agent struct {
ID int
URL string
Secret string
CheckPeriod int
}
type Config struct {
DatabasePath string `yaml:"database_path"`
HealthCheckEnabled bool `yaml:"health_check_enabled"`
HealthCheckPort int `yaml:"health_check_port"`
HealthCheckEndpoint string `yaml:"health_check_endpoint"`
}
var defaultConfig = Config {
DatabasePath: "./jilo-server.db",
HealthCheckEnabled: false,
HealthCheckPort: 8080,
HealthCheckEndpoint: "/health",
}
// Loading the config file
func readConfig(filePath string) Config {
config := defaultConfig
file, err := ioutil.ReadFile(filePath)
if err != nil {
log.Println("Can't read config file, using defaults.")
return config
}
err = yaml.Unmarshal(file, &config)
if err != nil {
log.Println("Can't parse the config file, using defaults.")
return config
}
return config
}
// Start the health check
func startHealthCheckServer(port int, endpoint string) {
http.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
// If the server is healthy, the response if 200 OK json, no content
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
})
address := fmt.Sprintf(":%d", port)
log.Printf("Starting health check server on %s%s", address, endpoint)
go http.ListenAndServe(address, nil)
}
// Database initialization
func setupDatabase(dbPath string, initDB bool) (*sql.DB, error) {
// Open the database
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return nil, err
}
// Check if the table exists
tableExists := checkTableExists(db)
if !tableExists && !initDB {
// Ask if we should create the table
fmt.Print("Table not found. Do you want to create it? (y/n): ")
var response string
fmt.Scanln(&response)
if response != "y" && response != "Y" {
log.Println("Exiting because the table is missing, but mandatory.")
os.Exit(1)
}
}
// If the table is not there, initialize it
createTable := `
CREATE TABLE IF NOT EXISTS jilo_agent_checks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id INTEGER,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
status_code INTEGER,
response_time_ms INTEGER,
response_content TEXT,
FOREIGN KEY(agent_id) REFERENCES jilo_agents(id)
);`
_, err = db.Exec(createTable)
if err != nil {
return nil, err
}
return db, nil
}
// Check for the table
func checkTableExists(db *sql.DB) bool {
sql := `SELECT
name
FROM
sqlite_master
WHERE
type='table'
AND
name='jilo_agent_checks';`
row := db.QueryRow(sql)
var name string
err := row.Scan(&name)
return err == nil && name == "jilo_agent_checks"
}
// Get Jilo agents details
func getAgents(db *sql.DB) ([]Agent, error) {
sql := `SELECT
ja.id,
ja.url,
COALESCE(ja.secret_key, '') AS secret_key,
COALESCE(ja.check_period, 0) AS check_period,
jat.endpoint
FROM
jilo_agents ja
JOIN
jilo_agent_types jat ON ja.agent_type_id = jat.id`
rows, err := db.Query(sql)
if err != nil {
return nil, err
}
defer rows.Close()
var agents []Agent
for rows.Next() {
var agent Agent
var endpoint string
var checkPeriod int
// Get the agent details
if err := rows.Scan(&agent.ID, &agent.URL, &agent.Secret, &checkPeriod, &endpoint); err != nil {
return nil, err
}
// Form the whole enpoint
agent.URL += endpoint
agent.CheckPeriod = checkPeriod
agents = append(agents, agent)
}
// We return the endpoints, not all the details
return agents, nil
}
// Format the enpoint URL to use in logs
func getDomainAndPath(fullURL string) string {
parsedURL, err := url.Parse(fullURL)
if err != nil {
// Fallback to the original URL on error
return fullURL
}
return parsedURL.Host + parsedURL.Path
}
// JWT token generation
func generateJWT(secret string) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iat": time.Now().Unix(),
})
tokenString, err := token.SignedString([]byte(secret))
if err != nil {
return "", err
}
return tokenString, nil
}
// Check agent endpoint
func checkEndpoint(agent Agent) (int, int64, string, bool) {
log.Println("Sending HTTP get request to Jilo agent:", agent.URL)
// Create the http request
req, err := http.NewRequest("GET", agent.URL, nil)
if err != nil {
log.Println("Failed to create the HTTP request:", err)
return 0, 0, "", false
}
// Generate the JWT token
if agent.Secret != "" {
token, err := generateJWT(agent.Secret)
if err != nil {
log.Println("Failed to generate JWT token:", err)
return 0, 0, "", false
}
// Set Authorization header
req.Header.Set("Authorization", "Bearer "+token)
}
start := time.Now()
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Println("Failed to check the endpoint:", err)
return 0, 0, "", false
}
defer resp.Body.Close()
elapsed := time.Since(start).Milliseconds()
// Read the response body
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println("Failed to read the response body:", err)
return resp.StatusCode, elapsed, "", false
}
log.Printf("Received response: %d, Time taken: %d ms", resp.StatusCode, elapsed)
return resp.StatusCode, elapsed, string(body), true
}
// Insert the checks into the database
func saveData(db *sql.DB, agentID int, statusCode int, responseTime int64, responseContent string) {
sql := `INSERT INTO
jilo_agent_checks
(agent_id, status_code, response_time_ms, response_content)
VALUES
(?, ?, ?, ?)`
_, err := db.Exec(sql, agentID, statusCode, responseTime, responseContent)
if err != nil {
log.Println("Failed to insert data into the database:", err)
}
}
// Main routine
func main() {
// First flush all the logs
log.SetFlags(log.LstdFlags | log.Lshortfile)
// Command-line options
// "--init-db" creates the table
initDB := flag.Bool("init-db", false, "Create database table if not present without prompting")
// Config file
configPath := flag.String("config", "", "Path to the configuration file (use -c or --config)")
flag.StringVar(configPath, "c", "", "Path to the configuration file")
flag.Parse()
// Choosing the config file
finalConfigPath := "./jilo-server.conf" // this is the default we fall to
if *configPath != "" {
if _, err := os.Stat(*configPath); err == nil {
finalConfigPath = *configPath
} else {
log.Printf("Specified file \"%s\" doesn't exist. Falling back to the default \"%s\".", *configPath, finalConfigPath)
}
}
// Config file
log.Printf("Using config file %s", finalConfigPath)
config := readConfig(finalConfigPath)
// Start the health check, if it's enabled in the config file
if config.HealthCheckEnabled {
startHealthCheckServer(config.HealthCheckPort, config.HealthCheckEndpoint)
}
// Connect to or setup the database
log.Println("Initializing the database...")
db, err := setupDatabase(config.DatabasePath, *initDB)
if err != nil {
log.Fatal("Failed to initialize the database:", err)
}
defer db.Close()
// Prepare the Agents
agents, err := getAgents(db)
if err != nil {
log.Fatal("Failed to fetch the agents:", err)
}
log.Println("Starting endpoint checker...")
// Iterate over the servers and agents
for _, agent := range agents {
if agent.CheckPeriod > 0 {
go func(agent Agent) {
// Ticker for the periodic checks
ticker := time.NewTicker(time.Duration(agent.CheckPeriod) * time.Minute)
defer ticker.Stop()
for {
log.Printf("Checking agent [%d]: %s", agent.ID, agent.URL)
statusCode, responseTime, responseContent, success := checkEndpoint(agent)
if success {
log.Printf("Agent [%d]: Status code: %d, Response time: %d ms", agent.ID, statusCode, responseTime)
saveData(db, agent.ID, statusCode, responseTime, responseContent)
} else {
log.Printf("Check for agent %s (%d) failed, skipping database insert", getDomainAndPath(agent.URL), agent.ID)
}
// Sleep until the next tick
<-ticker.C
}
}(agent)
} else {
log.Printf("Agent %s (%d) has an invalid CheckPeriod (%d), skipping it.", getDomainAndPath(agent.URL), agent.ID, agent.CheckPeriod)
}
}
// Prevent the main from exiting
select {}
}