-
Notifications
You must be signed in to change notification settings - Fork 17
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
Move filters' source of truth to the backend #148
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,55 @@ | ||||||
package server | ||||||
|
||||||
import ( | ||||||
"fmt" | ||||||
"reflect" | ||||||
"strings" | ||||||
) | ||||||
|
||||||
const ( | ||||||
INT = "int" | ||||||
UINT = "uint" | ||||||
STRING = "string" | ||||||
TIME = "time" | ||||||
ENUM = "enum" | ||||||
) | ||||||
|
||||||
type FieldMetaData struct { | ||||||
Name string `json:"name"` | ||||||
Type string `json:"type"` | ||||||
Values []string `json:"values"` | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to note, in this design, the
Comment on lines
+9
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. warning This all requires description. |
||||||
} | ||||||
|
||||||
type Description []FieldMetaData | ||||||
|
||||||
/* | ||||||
DescribeEntity returns a Description for some entity fields | ||||||
to help frontend renders appropiate filters and tables. | ||||||
it depends on the tags assigned to the entity fields to create the needed metadata. | ||||||
it expects tags: filter, values | ||||||
*/ | ||||||
Comment on lines
+25
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpicking
Would be nice to have an example of such entity (which is supposed to be used here). |
||||||
func DescribeEntity(entity interface{}) (Description, error) { | ||||||
var describtion Description | ||||||
t := reflect.TypeOf(entity) | ||||||
|
||||||
for i := 0; i < t.NumField(); i++ { | ||||||
sf := t.Field(i) | ||||||
if sf.Anonymous { | ||||||
return nil, fmt.Errorf("Error Anonymous Field") | ||||||
} | ||||||
|
||||||
name := sf.Tag.Get("json") | ||||||
tagValue := sf.Tag.Get("filter") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you figure out these data types automatically? you already have that access. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
the tag There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you should document this. it's not obvious. also not obvious how that map/string example works. |
||||||
var values []string | ||||||
if tagValue == ENUM { | ||||||
values = strings.Split(sf.Tag.Get("values"), ",") | ||||||
} | ||||||
describtion = append(describtion, FieldMetaData{ | ||||||
Name: name, | ||||||
Type: tagValue, | ||||||
Values: values, | ||||||
}) | ||||||
} | ||||||
|
||||||
return describtion, nil | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Comment on lines
+31
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. opinion I understand the initial idea (of why we decided to use reflection in the first place) is that this is generic logic (not coupled with the |
||||||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -22,9 +22,15 @@ var ( | |||||
DefaultDBAccessTimeout time.Duration = 10 * time.Second | ||||||
) | ||||||
|
||||||
/* | ||||||
Query should have the same names as the fields in the entity that it queries(in this case Log). | ||||||
To generalize filtering time fields (range filtering), it should have two fields with prefixes `start_<name>`, `end_<name>` | ||||||
to make the frontend generate the query programmatically. | ||||||
e.g. (Log.Date `date` -> Query.StartDate `start_date`, Query.EndDate `end_date`) | ||||||
*/ | ||||||
type Query struct { | ||||||
JobID *uint64 `form:"job_id"` | ||||||
Text *string `form:"text"` | ||||||
LogData *string `form:"log_data"` | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what was wrong with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the frontend will form the query dynamically based on the metadata that is sent from the backend. so it's easier ( for the frontend ) to make the query fields match the log struct fields. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i actually dont understand your answer. Are there hidden dependencies in this chain? Do you expect to have some field names matching stuff that it shouldn't? (based on your "it's easier for the frontend to make the query fields match") |
||||||
LogLevel *string `form:"log_level"` | ||||||
StartDate *time.Time `form:"start_date" time_format:"2006-01-02T15:04:05.000Z07:00"` | ||||||
EndDate *time.Time `form:"end_date" time_format:"2006-01-02T15:04:05.000Z07:00"` | ||||||
|
@@ -40,7 +46,7 @@ func (q *Query) ToStorageQuery() storage.Query { | |||||
} | ||||||
|
||||||
storageQuery.JobID = q.JobID | ||||||
storageQuery.Text = q.Text | ||||||
storageQuery.LogData = q.LogData | ||||||
storageQuery.LogLevel = q.LogLevel | ||||||
storageQuery.StartDate = q.StartDate | ||||||
storageQuery.EndDate = q.EndDate | ||||||
|
@@ -57,10 +63,10 @@ func (q *Query) ToStorageQuery() storage.Query { | |||||
} | ||||||
|
||||||
type Log struct { | ||||||
JobID uint64 `json:"job_id"` | ||||||
LogData string `json:"log_data"` | ||||||
Date time.Time `json:"date"` | ||||||
LogLevel string `json:"log_level"` | ||||||
JobID uint64 `json:"job_id" filter:"uint"` | ||||||
LogData string `json:"log_data" filter:"string"` | ||||||
Date time.Time `json:"date" filter:"time"` | ||||||
LogLevel string `json:"log_level" filter:"enum" values:"info,debug,error,fatal,panic,warning"` | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how do you make sure these hardcoded values actually match the data? what if the logged data has "INFORMATION" as level, for example; and you only give the user the option "info" as per this values tag There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. okay, i will make a db distinct query to get the values for the enum filters. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. make sure you understand what the cost of that is. you may end up hammering the db with metadata related requests, combined with missing indices and you probably get a whole server crash or something. |
||||||
} | ||||||
|
||||||
func (l *Log) ToStorageLog() storage.Log { | ||||||
|
@@ -159,6 +165,17 @@ func (r *RouteHandler) status(c *gin.Context) { | |||||
c.JSON(http.StatusOK, gin.H{"status": "live"}) | ||||||
} | ||||||
|
||||||
// | ||||||
func (r *RouteHandler) describeLog(c *gin.Context) { | ||||||
res, err := DescribeEntity(Log{}) | ||||||
if err != nil { | ||||||
c.JSON(http.StatusInternalServerError, gin.H{"status": "err", "msg": "error while getting the storage descirbtion"}) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
return | ||||||
} | ||||||
|
||||||
c.JSON(http.StatusOK, res) | ||||||
} | ||||||
|
||||||
// addLogs inserts log's batches into the database | ||||||
func (r *RouteHandler) addLogs(c *gin.Context) { | ||||||
var logs []Log | ||||||
|
@@ -277,6 +294,7 @@ func initRouter(ctx xcontext.Context, rh RouteHandler, middlewares []gin.Handler | |||||
r.GET("/log", rh.getLogs) | ||||||
r.GET("/tag", rh.getTags) | ||||||
r.GET("/tag/:name/jobs", rh.getJobs) | ||||||
r.GET("/log-description", rh.describeLog) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this probably sits better under |
||||||
|
||||||
// serve the frontend app | ||||||
r.StaticFS("/app", FS(false)) | ||||||
|
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,39 +1,36 @@ | ||
import superagent from 'superagent'; | ||
|
||
// TODO: remove the hardcoded levels | ||
// logLevels is the possible levels for logs | ||
export const Levels = ['panic', 'fatal', 'error', 'warning', 'info', 'debug']; | ||
|
||
// Log defines the expected log entry returned from the api | ||
export interface Log { | ||
job_id: number; | ||
log_data: string; | ||
log_level: string; | ||
date: string; | ||
} | ||
|
||
// Query defines all the possible filters to form a query | ||
export interface Query { | ||
job_id?: number; | ||
text?: string; | ||
log_level?: string; | ||
start_date?: string; | ||
end_date?: string; | ||
page: number; | ||
page_size: number; | ||
} | ||
|
||
// Result defines the structure of the api response to a query | ||
export interface Result { | ||
logs: Log[] | null; | ||
logs: any; | ||
count: number; | ||
page: number; | ||
page_size: number; | ||
} | ||
|
||
// getLogs returns Result that contains logs fetched according to the Query | ||
export async function getLogs(query: Query): Promise<Result> { | ||
export async function getLogs(query: any): Promise<Result> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. usage of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the query will be constructed based on the meta date that is fetched from the backend. i don't know how i can modify that to not use any ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess it should be an array of arbitrary fields+values instead of any. [just thinking out loud] I actually like GraphQL, because everything is already invented there and we do not need to reinvent a query language and reimplement it in there. But on the other hand it is much more complicated. |
||
let result: superagent.Response = await superagent.get('/log').query(query); | ||
|
||
return result.body; | ||
} | ||
|
||
export enum FieldType { | ||
INT = 'int', | ||
UINT = 'uint', | ||
STRING = 'string', | ||
TIME = 'time', | ||
ENUM = 'enum', | ||
} | ||
|
||
export interface FieldMetaData { | ||
name: string; | ||
type: string; | ||
values: string[]; | ||
} | ||
|
||
export async function getLogDescription(): Promise<FieldMetaData[]> { | ||
let result: superagent.Response = await superagent.get('/log-description'); | ||
|
||
return result.body; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import React from 'react'; | ||
import { FieldMetaData, FieldType } from '../../api/logs'; | ||
import { Column, Cell, HeaderCell } from 'rsuite-table'; | ||
import DateCell from './date_cell/date_cell'; | ||
|
||
export function renderColumn(field: FieldMetaData, key: any) { | ||
switch (field.type) { | ||
case FieldType.INT: | ||
case FieldType.UINT: | ||
case FieldType.ENUM: | ||
return ( | ||
<Column width={80} align="center" fixed key={key}> | ||
<HeaderCell>{field.name}</HeaderCell> | ||
<Cell className="log-table__cell" dataKey={field.name} /> | ||
</Column> | ||
); | ||
case FieldType.STRING: | ||
return ( | ||
<Column width={600} align="left" flexGrow={1} key={key}> | ||
<HeaderCell>{field.name}</HeaderCell> | ||
<Cell className="log-table__cell" dataKey={field.name} /> | ||
</Column> | ||
); | ||
case FieldType.TIME: | ||
return ( | ||
<Column width={250} align="center" fixed key={key}> | ||
<HeaderCell>{field.name}</HeaderCell> | ||
<DateCell | ||
className="log-table__cell" | ||
dataKey={field.name} | ||
/> | ||
</Column> | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
filename typo