mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-04-10 18:03:57 +00:00
Some checks failed
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in -30m5s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in -29m35s
Build , Vet Test, and Lint / Lint Code (push) Successful in -29m19s
Build , Vet Test, and Lint / Build (push) Successful in -29m43s
Tests / Integration Tests (push) Failing after -30m34s
Tests / Unit Tests (push) Successful in -30m8s
* implement fallbackMetricEntityFromQuery for query sanitization * add tests for fallback metric entity and sanitization logic
336 lines
7.2 KiB
Go
336 lines
7.2 KiB
Go
package database
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
|
"github.com/bitechdev/ResolveSpec/pkg/metrics"
|
|
"github.com/bitechdev/ResolveSpec/pkg/reflection"
|
|
)
|
|
|
|
const maxMetricFallbackEntityLength = 120
|
|
|
|
func recordQueryMetrics(enabled bool, operation, schema, entity, table string, startedAt time.Time, err error) {
|
|
if !enabled {
|
|
return
|
|
}
|
|
|
|
metrics.GetProvider().RecordDBQuery(
|
|
normalizeMetricOperation(operation),
|
|
normalizeMetricSchema(schema),
|
|
normalizeMetricEntity(entity, table),
|
|
normalizeMetricTable(table),
|
|
time.Since(startedAt),
|
|
err,
|
|
)
|
|
}
|
|
|
|
func normalizeMetricOperation(operation string) string {
|
|
operation = strings.ToUpper(strings.TrimSpace(operation))
|
|
if operation == "" {
|
|
return "UNKNOWN"
|
|
}
|
|
return operation
|
|
}
|
|
|
|
func normalizeMetricSchema(schema string) string {
|
|
schema = cleanMetricIdentifier(schema)
|
|
if schema == "" {
|
|
return "default"
|
|
}
|
|
return schema
|
|
}
|
|
|
|
func normalizeMetricEntity(entity, table string) string {
|
|
entity = cleanMetricIdentifier(entity)
|
|
if entity != "" {
|
|
return entity
|
|
}
|
|
|
|
table = cleanMetricIdentifier(table)
|
|
if table != "" {
|
|
return table
|
|
}
|
|
|
|
return "unknown"
|
|
}
|
|
|
|
func normalizeMetricTable(table string) string {
|
|
table = cleanMetricIdentifier(table)
|
|
if table == "" {
|
|
return "unknown"
|
|
}
|
|
return table
|
|
}
|
|
|
|
func entityNameFromModel(model interface{}, table string) string {
|
|
if model == nil {
|
|
return cleanMetricIdentifier(table)
|
|
}
|
|
|
|
modelType := reflect.TypeOf(model)
|
|
for modelType != nil && (modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
|
|
modelType = modelType.Elem()
|
|
}
|
|
|
|
if modelType == nil {
|
|
return cleanMetricIdentifier(table)
|
|
}
|
|
|
|
if modelType.Kind() == reflect.Struct && modelType.Name() != "" {
|
|
return reflection.ToSnakeCase(modelType.Name())
|
|
}
|
|
|
|
return cleanMetricIdentifier(table)
|
|
}
|
|
|
|
func schemaAndTableFromModel(model interface{}, driverName string) (schema, table string) {
|
|
provider, ok := tableNameProviderFromModel(model)
|
|
if !ok {
|
|
return "", ""
|
|
}
|
|
|
|
return parseTableName(provider.TableName(), driverName)
|
|
}
|
|
|
|
// tableNameProviderType is cached to avoid repeated reflection on every call.
|
|
var tableNameProviderType = reflect.TypeOf((*common.TableNameProvider)(nil)).Elem()
|
|
|
|
func tableNameProviderFromModel(model interface{}) (common.TableNameProvider, bool) {
|
|
if model == nil {
|
|
return nil, false
|
|
}
|
|
|
|
if provider, ok := model.(common.TableNameProvider); ok {
|
|
return provider, true
|
|
}
|
|
|
|
modelType := reflect.TypeOf(model)
|
|
for modelType != nil && (modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
|
|
modelType = modelType.Elem()
|
|
}
|
|
|
|
if modelType == nil || modelType.Kind() != reflect.Struct {
|
|
return nil, false
|
|
}
|
|
|
|
// Check whether *T implements TableNameProvider before allocating.
|
|
ptrType := reflect.PointerTo(modelType)
|
|
if !ptrType.Implements(tableNameProviderType) && !modelType.Implements(tableNameProviderType) {
|
|
return nil, false
|
|
}
|
|
|
|
modelValue := reflect.New(modelType)
|
|
if provider, ok := modelValue.Interface().(common.TableNameProvider); ok {
|
|
return provider, true
|
|
}
|
|
|
|
if provider, ok := modelValue.Elem().Interface().(common.TableNameProvider); ok {
|
|
return provider, true
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
func metricTargetFromRawQuery(query, driverName string) (operation, schema, entity, table string) {
|
|
operation = normalizeMetricOperation(firstQueryKeyword(query))
|
|
tableRef := tableFromRawQuery(query, operation)
|
|
if tableRef == "" {
|
|
return operation, "", fallbackMetricEntityFromQuery(query), "unknown"
|
|
}
|
|
|
|
schema, table = parseTableName(tableRef, driverName)
|
|
entity = cleanMetricIdentifier(table)
|
|
return operation, schema, entity, table
|
|
}
|
|
|
|
func fallbackMetricEntityFromQuery(query string) string {
|
|
query = sanitizeMetricQueryShape(query)
|
|
if query == "" {
|
|
return "unknown"
|
|
}
|
|
|
|
if len(query) > maxMetricFallbackEntityLength {
|
|
return query[:maxMetricFallbackEntityLength-3] + "..."
|
|
}
|
|
|
|
return query
|
|
}
|
|
|
|
func sanitizeMetricQueryShape(query string) string {
|
|
query = strings.TrimSpace(query)
|
|
if query == "" {
|
|
return ""
|
|
}
|
|
|
|
var out strings.Builder
|
|
for i := 0; i < len(query); {
|
|
if query[i] == '\'' {
|
|
out.WriteByte('?')
|
|
i++
|
|
for i < len(query) {
|
|
if query[i] == '\'' {
|
|
if i+1 < len(query) && query[i+1] == '\'' {
|
|
i += 2
|
|
continue
|
|
}
|
|
i++
|
|
break
|
|
}
|
|
i++
|
|
}
|
|
continue
|
|
}
|
|
|
|
if query[i] == '?' {
|
|
out.WriteByte('?')
|
|
i++
|
|
continue
|
|
}
|
|
|
|
if query[i] == '$' && i+1 < len(query) && isASCIIDigit(query[i+1]) {
|
|
out.WriteByte('?')
|
|
i++
|
|
for i < len(query) && isASCIIDigit(query[i]) {
|
|
i++
|
|
}
|
|
continue
|
|
}
|
|
|
|
if query[i] == ':' && (i == 0 || query[i-1] != ':') && i+1 < len(query) && isIdentifierStart(query[i+1]) {
|
|
out.WriteByte('?')
|
|
i++
|
|
for i < len(query) && isIdentifierPart(query[i]) {
|
|
i++
|
|
}
|
|
continue
|
|
}
|
|
|
|
if query[i] == '@' && (i == 0 || query[i-1] != '@') && i+1 < len(query) && isIdentifierStart(query[i+1]) {
|
|
out.WriteByte('?')
|
|
i++
|
|
for i < len(query) && isIdentifierPart(query[i]) {
|
|
i++
|
|
}
|
|
continue
|
|
}
|
|
|
|
if startsNumericLiteral(query, i) {
|
|
out.WriteByte('?')
|
|
i++
|
|
for i < len(query) && (isASCIIDigit(query[i]) || query[i] == '.') {
|
|
i++
|
|
}
|
|
continue
|
|
}
|
|
|
|
out.WriteByte(query[i])
|
|
i++
|
|
}
|
|
|
|
return strings.Join(strings.Fields(out.String()), " ")
|
|
}
|
|
|
|
func startsNumericLiteral(query string, idx int) bool {
|
|
if idx >= len(query) {
|
|
return false
|
|
}
|
|
|
|
start := idx
|
|
if query[idx] == '-' {
|
|
if idx+1 >= len(query) || !isASCIIDigit(query[idx+1]) {
|
|
return false
|
|
}
|
|
start++
|
|
}
|
|
|
|
if !isASCIIDigit(query[start]) {
|
|
return false
|
|
}
|
|
|
|
if idx > 0 && isIdentifierPart(query[idx-1]) {
|
|
return false
|
|
}
|
|
|
|
if start+1 < len(query) && query[start] == '0' && (query[start+1] == 'x' || query[start+1] == 'X') {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func isASCIIDigit(ch byte) bool {
|
|
return ch >= '0' && ch <= '9'
|
|
}
|
|
|
|
func isIdentifierStart(ch byte) bool {
|
|
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_'
|
|
}
|
|
|
|
func isIdentifierPart(ch byte) bool {
|
|
return isIdentifierStart(ch) || isASCIIDigit(ch)
|
|
}
|
|
|
|
func firstQueryKeyword(query string) string {
|
|
query = strings.TrimSpace(query)
|
|
if query == "" {
|
|
return ""
|
|
}
|
|
|
|
fields := strings.Fields(query)
|
|
if len(fields) == 0 {
|
|
return ""
|
|
}
|
|
|
|
return fields[0]
|
|
}
|
|
|
|
func tableFromRawQuery(query, operation string) string {
|
|
tokens := tokenizeQuery(query)
|
|
if len(tokens) == 0 {
|
|
return ""
|
|
}
|
|
|
|
switch operation {
|
|
case "SELECT":
|
|
return tokenAfter(tokens, "FROM")
|
|
case "INSERT":
|
|
return tokenAfter(tokens, "INTO")
|
|
case "UPDATE":
|
|
return tokenAfter(tokens, "UPDATE")
|
|
case "DELETE":
|
|
return tokenAfter(tokens, "FROM")
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func tokenAfter(tokens []string, keyword string) string {
|
|
for idx, token := range tokens {
|
|
if strings.EqualFold(token, keyword) && idx+1 < len(tokens) {
|
|
return cleanMetricIdentifier(tokens[idx+1])
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func tokenizeQuery(query string) []string {
|
|
replacer := strings.NewReplacer(
|
|
"\n", " ",
|
|
"\t", " ",
|
|
"(", " ",
|
|
")", " ",
|
|
",", " ",
|
|
)
|
|
return strings.Fields(replacer.Replace(query))
|
|
}
|
|
|
|
func cleanMetricIdentifier(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
value = strings.Trim(value, "\"'`[]")
|
|
value = strings.TrimRight(value, ";")
|
|
return value
|
|
}
|