Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce parser field list #5

Merged
merged 20 commits into from
Jul 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 0 additions & 163 deletions accesslog.go

This file was deleted.

17 changes: 6 additions & 11 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ type dbSession struct {
insertStmt *sql.Stmt
}

const DB_DATE_LAYOUT = "2006-01-02 15:04:05-07:00"

// Open or create the database at the given path.
func InitDB(dbPath string) (*dbSession, error) {
db, err := sql.Open("sqlite3", dbPath)
Expand All @@ -36,17 +38,17 @@ func InitDB(dbPath string) (*dbSession, error) {
}

// TODO consider adding indexes according to expected queries

// FIXME build this dynamically based on the log format columns
sqlStmt := `
CREATE TABLE IF NOT EXISTS access_logs (
id INTEGER NOT NULL PRIMARY KEY,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

ip TEXT,
time TIMESTAMP NOT NULL,
request_raw TEXT NOT NULL,
user_agent_raw TEXT,
status INTEGER,
bytes_sent INTEGER,
referer TEXT COLLATE NOCASE,

method TEXT COLLATE NOCASE,
Expand All @@ -55,10 +57,8 @@ func InitDB(dbPath string) (*dbSession, error) {
os TEXT COLLATE NOCASE,
device TEXT COLLATE NOCASE,
ua_url TEXT,
ua_type TEXT COLLATE NOCASE,

ua_type TEXT COLLATE NOCASE

created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`
_, err = db.Exec(sqlStmt)
Expand Down Expand Up @@ -86,7 +86,7 @@ func (dbs *dbSession) PrepareForUpdate(columns []string) (*time.Time, error) {
return nil, err
}

t, _ := timeFromDBFormat(lastSeenTimeStr)
t, _ := time.Parse(DB_DATE_LAYOUT, lastSeenTimeStr)
lastSeemTime = &t
}

Expand Down Expand Up @@ -217,8 +217,3 @@ func (spec *RequestCountSpec) buildQuery() (string, []any) {

return queryString, queryArgs
}

func timeFromDBFormat(timestamp string) (time.Time, error) {
sqliteLayout := "2006-01-02 15:04:05-07:00"
return time.Parse(sqliteLayout, timestamp)
}
51 changes: 18 additions & 33 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,6 @@ type CommandArgs struct {
Where []string `short:"w" optional:"" help:"Filter expressions. Example: -w useragent=Safari -w status=200"`
}

// FIXME consolidate field list (duplicated knowledge)
var FIELD_NAMES = map[string]string{
"user_agent": "user_agent",
"useragent": "user_agent",
"ua": "user_agent",
"ua_type": "ua_type",
"uatype": "ua_type",
"ua_url": "ua_url",
"uaurl": "ua_url",
"os": "os",
"device": "device",
"request": "request_raw",
"bytes": "bytes_sent",
"bytes_sent": "bytes_sent",
"path": "path",
"url": "path",
"ip": "ip",
"referer": "referer",
"referrer": "referer",
"status": "status",
"method": "method",
}

// Use a var to get current time, allowing for tests to override it
var NowTimeFun = time.Now

Expand All @@ -55,6 +32,9 @@ var NowTimeFun = time.Now
const DEFAULT_PATH_PATTERN = "/var/log/nginx/access.log*"
const DEFAULT_DB_PATH = "./ngtop.db"

// TODO replace with 'combined' once alias support is added
const DEFAULT_LOG_FORMAT = `$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"`

func main() {
// Optionally enable internal logger
if os.Getenv("NGTOP_LOG") == "" {
Expand All @@ -71,12 +51,17 @@ func main() {
logPathPattern = envLogsPath
}

logFormat := DEFAULT_LOG_FORMAT
if envLogFormat := os.Getenv("NGTOP_LOG_FORMAT"); envLogFormat != "" {
logFormat = envLogFormat
}

ctx, spec := querySpecFromCLI()
dbs, err := InitDB(dbPath)
ctx.FatalIfErrorf(err)
defer dbs.Close()

err = loadLogs(logPathPattern, dbs)
err = loadLogs(logFormat, logPathPattern, dbs)
ctx.FatalIfErrorf(err)

columnNames, rowValues, err := dbs.QueryTop(spec)
Expand All @@ -87,8 +72,8 @@ func main() {
// Parse the command line arguments into a top requests query specification
func querySpecFromCLI() (*kong.Context, *RequestCountSpec) {
// Parse query spec first, i.e. don't bother with db updates if the command is invalid
fieldNames := make([]string, 0, len(FIELD_NAMES))
for k := range FIELD_NAMES {
fieldNames := make([]string, 0, len(CLI_NAME_TO_FIELD))
for k := range CLI_NAME_TO_FIELD {
fieldNames = append(fieldNames, k)
}

Expand All @@ -111,7 +96,7 @@ func querySpecFromCLI() (*kong.Context, *RequestCountSpec) {
// translate field name aliases
columns := make([]string, len(cli.Fields))
for i, field := range cli.Fields {
columns[i] = FIELD_NAMES[field]
columns[i] = CLI_NAME_TO_FIELD[field].ColumnName
}

whereConditions, err := resolveWhereConditions(cli.Where)
Expand Down Expand Up @@ -144,10 +129,10 @@ func resolveWhereConditions(clauses []string) (map[string][]string, error) {
return nil, fmt.Errorf("invalid where expression %s", clause)
}

if column, found := FIELD_NAMES[keyvalue[0]]; !found {
return nil, fmt.Errorf("unknown field name %s", keyvalue[0])
if field, found := CLI_NAME_TO_FIELD[keyvalue[0]]; found {
conditions[field.ColumnName] = append(conditions[field.ColumnName], keyvalue[1])
} else {
conditions[column] = append(conditions[column], keyvalue[1])
return nil, fmt.Errorf("unknown field name %s", keyvalue[0])
}
}

Expand Down Expand Up @@ -187,21 +172,21 @@ func parseDuration(duration string) (time.Time, error) {
}

// Parse the most recent nginx access.logs and insert the ones not previously seen into the DB.
func loadLogs(logPathPattern string, dbs *dbSession) error {
func loadLogs(logFormat string, logPathPattern string, dbs *dbSession) error {
logFiles, err := filepath.Glob(logPathPattern)
if err != nil {
return err
}

// FIXME consolidate field list (duplicated knowledge)
dbColumns := []string{"ip", "time", "request_raw", "status", "bytes_sent", "referer", "user_agent_raw", "method", "path", "user_agent", "os", "device", "ua_url", "ua_type"}
dbColumns := []string{"ip", "time", "request_raw", "status", "referer", "user_agent_raw", "method", "path", "user_agent", "os", "device", "ua_url", "ua_type"}

lastSeenTime, err := dbs.PrepareForUpdate(dbColumns)
if err != nil {
return err
}

err = ProcessAccessLogs(logFiles, lastSeenTime, func(logLineFields map[string]interface{}) error {
err = ProcessAccessLogs(logFormat, logFiles, lastSeenTime, func(logLineFields map[string]string) error {
queryValues := make([]interface{}, len(dbColumns))
for i, field := range dbColumns {
queryValues[i] = logLineFields[field]
Expand Down
2 changes: 1 addition & 1 deletion main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ func runCommand(t *testing.T, logs string, cliArgs []string) ([]string, [][]stri
assertEqual(t, err, nil)
defer dbs.Close()

err = loadLogs(logFile.Name(), dbs)
err = loadLogs(DEFAULT_LOG_FORMAT, logFile.Name(), dbs)
assertEqual(t, err, nil)
columnNames, rowValues, err := dbs.QueryTop(spec)
assertEqual(t, err, nil)
Expand Down
Loading