fix: 验收修正

This commit is contained in:
zxr
2026-09-21 10:46:34 +08:00
parent b0ffde3100
commit 972adb3d38
12 changed files with 506 additions and 168 deletions

View File

@@ -241,6 +241,10 @@ func CreateTrapShield(ctx *gin.Context) {
infra.Response.Error(ctx, err)
return
}
if err := validateTrapShield(&row); err != nil {
infra.Response.Error(ctx, err)
return
}
row.ID = 0
if err := impl.DBService.Create(&row).Error; err != nil {
infra.Response.Error(ctx, err)
@@ -266,6 +270,10 @@ func UpdateTrapShield(ctx *gin.Context) {
return
}
row.ID = id
if err := validateTrapShield(&row); err != nil {
infra.Response.Error(ctx, err)
return
}
if err := impl.DBService.Save(&row).Error; err != nil {
infra.Response.Error(ctx, err)
return

View File

@@ -3,12 +3,16 @@ package controllers
import (
"encoding/json"
"fmt"
"net"
"regexp"
"strings"
"git.apinb.com/ops/logs/internal/ingest"
"git.apinb.com/ops/logs/internal/models"
)
var trapOIDPattern = regexp.MustCompile(`^\d+(?:\.\d+)*$`)
func validateSyslogRule(rule *models.SyslogRule) error {
regexFields := []struct {
name string
@@ -71,6 +75,55 @@ func validateTrapRule(rule *models.TrapRule) error {
return nil
}
func validateTrapShield(shield *models.TrapShield) error {
shield.Name = strings.TrimSpace(shield.Name)
shield.SourceIPCIDR = strings.TrimSpace(shield.SourceIPCIDR)
shield.InterfaceHint = strings.TrimSpace(shield.InterfaceHint)
shield.TimeWindowsJSON = strings.TrimSpace(shield.TimeWindowsJSON)
if shield.Name == "" {
return fmt.Errorf("Trap 屏蔽规则名称不能为空")
}
if shield.SourceIPCIDR != "" {
if strings.Contains(shield.SourceIPCIDR, "/") {
if _, _, err := net.ParseCIDR(shield.SourceIPCIDR); err != nil {
return fmt.Errorf("源 IP / CIDR 格式无效:%v", err)
}
} else if net.ParseIP(shield.SourceIPCIDR) == nil {
return fmt.Errorf("源 IP / CIDR 格式无效")
}
}
normalizedPrefixes := make([]string, 0, len(shield.OIDPrefixes))
seenPrefixes := make(map[string]struct{}, len(shield.OIDPrefixes))
for _, raw := range shield.OIDPrefixes {
prefix := normalizeTrapOIDInput(raw)
if prefix == "" {
continue
}
if !trapOIDPattern.MatchString(prefix) {
return fmt.Errorf("Trap OID %q 格式无效", raw)
}
if _, exists := seenPrefixes[prefix]; exists {
continue
}
seenPrefixes[prefix] = struct{}{}
normalizedPrefixes = append(normalizedPrefixes, prefix)
}
shield.OIDPrefixes = normalizedPrefixes
if shield.SourceIPCIDR == "" && len(shield.OIDPrefixes) == 0 && shield.InterfaceHint == "" {
return fmt.Errorf("Trap 屏蔽范围不能为空;请至少设置源 IP / CIDR、Trap 类型或接口提示中的一项")
}
if err := ingest.ValidateTimeWindowsJSON(shield.TimeWindowsJSON); err != nil {
return err
}
return nil
}
func normalizeTrapOIDInput(value string) string {
return strings.Trim(strings.TrimSpace(value), ".")
}
func validateOptionalRegex(field, pattern string) error {
pattern = strings.TrimSpace(pattern)
if pattern == "" {

View File

@@ -0,0 +1,82 @@
package controllers
import (
"strings"
"git.apinb.com/bsm-sdk/core/infra"
"git.apinb.com/ops/logs/internal/impl"
"git.apinb.com/ops/logs/internal/models"
"github.com/gin-gonic/gin"
)
type trapShieldOption struct {
OIDPrefix string `json:"oid_prefix"`
Name string `json:"name"`
Vendor string `json:"vendor"`
Source string `json:"source"`
}
// ListTrapShieldOptions 返回 Trap 字典和已接收 Trap 中可用于屏蔽配置的类型。
func ListTrapShieldOptions(ctx *gin.Context) {
var dictionary []models.TrapDictionaryEntry
if err := impl.DBService.Order("vendor asc, name asc, id asc").Find(&dictionary).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
options := make([]trapShieldOption, 0, len(dictionary))
seen := make(map[string]struct{}, len(dictionary))
for _, entry := range dictionary {
oid := normalizeTrapOIDInput(entry.OID)
if oid == "" {
oid = normalizeTrapOIDInput(entry.OIDPrefix)
}
if oid == "" {
continue
}
if _, exists := seen[oid]; exists {
continue
}
seen[oid] = struct{}{}
name := strings.TrimSpace(entry.Name)
if name == "" {
name = strings.TrimSpace(entry.Title)
}
if name == "" {
name = oid
}
options = append(options, trapShieldOption{
OIDPrefix: oid,
Name: name,
Vendor: strings.TrimSpace(entry.Vendor),
Source: "dictionary",
})
}
var receivedOIDs []string
if err := impl.DBService.Model(&models.LogEvent{}).
Where("source_kind = ? AND trap_o_id <> ?", "snmp_trap", "").
Distinct("trap_o_id").
Order("trap_o_id asc").
Pluck("trap_o_id", &receivedOIDs).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
for _, rawOID := range receivedOIDs {
oid := normalizeTrapOIDInput(rawOID)
if oid == "" {
continue
}
if _, exists := seen[oid]; exists {
continue
}
seen[oid] = struct{}{}
options = append(options, trapShieldOption{
OIDPrefix: oid,
Name: "已接收 Trap",
Source: "received",
})
}
infra.Response.Success(ctx, gin.H{"items": options})
}

View File

@@ -128,7 +128,8 @@ func ReceiveLogs(ctx *gin.Context) {
}
rows, rejected, message := decodeLogs(request)
if len(rows) > 0 {
if err := impl.DBService.CreateInBatches(rows, 500).Error; err != nil {
if err := impl.DBService.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "ingest_key"}}, DoNothing: true}).
CreateInBatches(rows, 500).Error; err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Log 入库失败"})
return
}
@@ -282,8 +283,12 @@ func logModel(input logPayload, resourceAttrs map[string]interface{}) (models.Lo
}
serviceName := limitCharacters(textAttribute(resourceAttrs, "service.name"), 512)
resourceUID := limitCharacters(firstTextAttribute(resourceAttrs, attrs, "resource.uid", "resource_uid"), 255)
var ingestKey *string
if value := limitCharacters(firstTextAttribute(attrs, nil, "log.record.uid"), 160); value != "" {
ingestKey = &value
}
return models.LogEvent{
CreatedAt: timeFromLog(input), SourceKind: "otlp", RawPayload: body, NormalizedSummary: body,
IngestKey: ingestKey, CreatedAt: timeFromLog(input), SourceKind: "otlp", RawPayload: body, NormalizedSummary: body,
NormalizedDetail: string(detailJSON), DeviceName: serviceName, ResourceType: "service", ResourceUID: resourceUID,
ResourceID: resourceUID, ResourceName: serviceName, BusinessSystemID: uintAttribute(resourceAttrs, attrs, "business.system.id", "business_system_id"),
TraceID: traceID, SpanID: spanID, MatchMethod: "otlp", DispatchStatus: "not_applicable",

View File

@@ -1,8 +1,11 @@
package otlp
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sort"
"strconv"
"strings"
@@ -41,6 +44,19 @@ type dependencyAggregate struct {
DurationMs float64
}
type middlewareCandidate struct {
CandidateKey string `json:"candidate_key"`
Product string `json:"product"`
DisplayName string `json:"display_name"`
Address string `json:"address,omitempty"`
SourceService string `json:"source_service"`
BusinessSystemID uint `json:"business_system_id"`
FirstSeenAt time.Time `json:"first_seen_at"`
LastSeenAt time.Time `json:"last_seen_at"`
SpanCount int64 `json:"span_count"`
ErrorCount int64 `json:"error_count"`
}
func ListTraces(ctx *gin.Context) {
page, size := pageParams(ctx)
query := applyTraceFilters(impl.DBService.Model(&models.TraceSpan{}), ctx)
@@ -128,6 +144,102 @@ func ListDependencies(ctx *gin.Context) {
infra.Response.Success(ctx, gin.H{"items": edges, "count": len(edges)})
}
// ListMiddlewareCandidates 从调用链标准属性中识别尚待管理员确认的中间件候选,不自动创建设备。
func ListMiddlewareCandidates(ctx *gin.Context) {
businessSystemID, err := strconv.ParseUint(strings.TrimSpace(ctx.Query("business_system_id")), 10, 32)
if err != nil || businessSystemID == 0 {
infra.Response.Error(ctx, errors.New("business_system_id 无效"))
return
}
start := time.Now().UTC().Add(-30 * 24 * time.Hour)
if value, parseErr := time.Parse(time.RFC3339, strings.TrimSpace(ctx.Query("start_time"))); parseErr == nil {
start = value.UTC()
}
var rows []models.TraceSpan
if err := impl.DBService.Where("business_system_id = ? AND start_time >= ?", uint(businessSystemID), start).
Order("start_time DESC").Limit(100000).Find(&rows).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
aggregates := make(map[string]*middlewareCandidate)
for _, row := range rows {
product, displayName, address, ok := middlewareIdentity(row)
if !ok {
continue
}
key := middlewareCandidateKey(uint(businessSystemID), product, displayName, address)
item := aggregates[key]
if item == nil {
item = &middlewareCandidate{
CandidateKey: key, Product: product, DisplayName: displayName, Address: address,
SourceService: row.ServiceName, BusinessSystemID: uint(businessSystemID),
FirstSeenAt: row.StartTime, LastSeenAt: row.StartTime,
}
aggregates[key] = item
}
item.SpanCount++
if row.StatusCode == "error" {
item.ErrorCount++
}
if row.StartTime.Before(item.FirstSeenAt) {
item.FirstSeenAt = row.StartTime
}
if row.StartTime.After(item.LastSeenAt) {
item.LastSeenAt = row.StartTime
item.SourceService = row.ServiceName
}
}
items := make([]middlewareCandidate, 0, len(aggregates))
for _, item := range aggregates {
items = append(items, *item)
}
sort.Slice(items, func(left, right int) bool {
if !items[left].LastSeenAt.Equal(items[right].LastSeenAt) {
return items[left].LastSeenAt.After(items[right].LastSeenAt)
}
return items[left].CandidateKey < items[right].CandidateKey
})
infra.Response.Success(ctx, gin.H{"items": items, "count": len(items), "start_time": start})
}
func middlewareIdentity(row models.TraceSpan) (string, string, string, bool) {
attrs := make(map[string]interface{})
resourceAttrs := make(map[string]interface{})
_ = json.Unmarshal([]byte(row.Attributes), &attrs)
_ = json.Unmarshal([]byte(row.ResourceAttrs), &resourceAttrs)
rawProduct := firstTextAttribute(attrs, resourceAttrs, "messaging.system", "db.system.name", "db.system", "peer.service")
product := knownMiddlewareProduct(rawProduct)
if product == "" {
return "", "", "", false
}
displayName := firstTextAttribute(attrs, resourceAttrs, "peer.service", "server.address", "network.peer.address", "net.peer.name")
if displayName == "" {
displayName = product
}
address := firstTextAttribute(attrs, resourceAttrs, "server.address", "network.peer.address", "net.peer.name")
port := firstTextAttribute(attrs, resourceAttrs, "server.port", "network.peer.port", "net.peer.port")
if address != "" && port != "" && !strings.Contains(address, ":") {
address += ":" + port
}
return product, limitCharacters(displayName, 255), limitCharacters(address, 255), true
}
func knownMiddlewareProduct(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
for _, product := range []string{"nginx", "apache", "tomcat", "redis", "kafka", "rabbitmq", "elasticsearch", "activemq", "rocketmq", "websphere"} {
if strings.Contains(value, product) {
return product
}
}
return ""
}
func middlewareCandidateKey(businessSystemID uint, product, name, address string) string {
raw := fmt.Sprintf("%d\x00%s\x00%s\x00%s", businessSystemID, product, strings.ToLower(name), strings.ToLower(address))
digest := sha256.Sum256([]byte(raw))
return hex.EncodeToString(digest[:])
}
func addDependency(aggregates map[string]*dependencyAggregate, source, target string, span models.TraceSpan) {
key := source + "\x00" + target
item := aggregates[key]