From b7e605fbfe74e886716273423b5d7904ddf0557a Mon Sep 17 00:00:00 2001 From: chris-sun-star <85611200+chris-sun-star@users.noreply.github.com> Date: Wed, 12 Jun 2024 15:59:06 +0800 Subject: [PATCH] update config in charts, support specify id when upgrade route (#437) --- .../templates/alertmanager-config.yaml | 1 - .../oceanbase-dashboard/templates/bundle.yaml | 2 + .../templates/prom-rule-config.yaml | 50 +++++++++---------- charts/oceanbase-dashboard/values.yaml | 3 +- internal/dashboard/business/alarm/route.go | 4 +- .../dashboard/generated/bindata/bindata.go | 44 ++++++++-------- internal/dashboard/handler/alarm_handler.go | 4 +- internal/dashboard/model/alarm/route/route.go | 5 ++ 8 files changed, 59 insertions(+), 54 deletions(-) diff --git a/charts/oceanbase-dashboard/templates/alertmanager-config.yaml b/charts/oceanbase-dashboard/templates/alertmanager-config.yaml index 5147ec01a..61aa62478 100644 --- a/charts/oceanbase-dashboard/templates/alertmanager-config.yaml +++ b/charts/oceanbase-dashboard/templates/alertmanager-config.yaml @@ -13,7 +13,6 @@ data: group_wait: 30s group_interval: 5m repeat_interval: 1h - continue: true receiver: 'local' receivers: - name: 'local' diff --git a/charts/oceanbase-dashboard/templates/bundle.yaml b/charts/oceanbase-dashboard/templates/bundle.yaml index 97abb407f..c9f457825 100644 --- a/charts/oceanbase-dashboard/templates/bundle.yaml +++ b/charts/oceanbase-dashboard/templates/bundle.yaml @@ -39,6 +39,8 @@ spec: value: {{ .Values.userCredentials | default (nospace (cat .Release.Name "-user-credentials")) }} - name: USER_NAMESPACE value: {{ .Values.userNamespace | default .Release.Namespace }} + - name: CONFIG_NAMESPACE + value: {{ .Values.configNamespace | default .Release.Namespace }} - name: PROMETHEUS_CONFIG value: {{ .Release.Name }}-prometheus-server-conf - name: PROMETHEUS_RULE_CONFIG diff --git a/charts/oceanbase-dashboard/templates/prom-rule-config.yaml b/charts/oceanbase-dashboard/templates/prom-rule-config.yaml index b2bd49757..c8c06435f 100644 --- a/charts/oceanbase-dashboard/templates/prom-rule-config.yaml +++ b/charts/oceanbase-dashboard/templates/prom-rule-config.yaml @@ -8,7 +8,7 @@ metadata: name: {{ .Release.Name }}-prometheus-rules-conf data: prometheus.rules: |- - groups: + {{`groups: - name: ob-rule rules: - alert: cluster_active_session @@ -18,7 +18,7 @@ data: instance_type: obcluster rule_name: cluster_active_session rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Cluster {{ $labels.ob_cluster_name }} has {{ $value }} active sessions on observer {{ $labels.svr_ip }}.' summary: 'Too much active sessions for cluster {{ $labels.ob_cluster_name }}.' @@ -29,7 +29,7 @@ data: instance_type: obcluster rule_name: inactive_server rule_type: builtin - serverity: critical + severity: critical annotations: description: 'Cluster {{ $labels.ob_cluster_name }} has {{ $value }} inactive observers.' summary: 'Found inactive observer in cluster {{ $labels.ob_cluster_name }}.' @@ -40,7 +40,7 @@ data: instance_type: obcluster rule_name: index_fail rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Cluster {{ $labels.ob_cluster_name }} has {{ $value }} index fail tables.' summary: 'Found index fail table in cluster {{ $labels.ob_cluster_name }}.' @@ -51,7 +51,7 @@ data: instance_type: obcluster rule_name: frozen_version_check rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Cluster {{ $labels.ob_cluster_name }} has {{ $value }} delta versions between merged and frozen data.' summary: 'Frozen version is too much larger than merged version.' @@ -62,7 +62,7 @@ data: instance_type: obcluster rule_name: cluster_merge_error rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Cluster {{ $labels.ob_cluster_name }} merge error.' summary: 'Cluster {{ $labels.ob_cluster_name }} merge error.' @@ -73,7 +73,7 @@ data: instance_type: obcluster rule_name: cluster_merge_timeout rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Cluster {{ $labels.ob_cluster_name }} merge timeout.' summary: 'Cluster {{ $labels.ob_cluster_name }} merge timeout.' @@ -84,7 +84,7 @@ data: instance_type: obcluster rule_name: cluster_no_frozen rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Cluster {{ $labels.ob_cluster_name }} has not frozen for {{ $value }} seconds.' summary: 'Cluster {{ $labels.ob_cluster_name }} has not frozen for a long time.' @@ -95,7 +95,7 @@ data: instance_type: obcluster rule_name: cluster_no_merge rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Cluster {{ $labels.ob_cluster_name }} has not merge for {{ $value }} seconds.' summary: 'Cluster {{ $labels.ob_cluster_name }} has not merge for a long time.' @@ -106,7 +106,7 @@ data: instance_type: obtenant rule_name: tenant_active_session rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} has {{ $value }} active sessions on observer {{ $labels.svr_ip }}.' summary: 'Too much active sessions for tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }}.' @@ -117,7 +117,7 @@ data: instance_type: obtenant rule_name: tenant_compaction_error rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} compaction error.' summary: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} compaction error.' @@ -128,7 +128,7 @@ data: instance_type: obtenant rule_name: tenant_cpu_usage rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} on observer {{ $labels.svr_ip }} cpu usage {{ $value }}%.' summary: 'High cpu usage detected for tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }}.' @@ -139,7 +139,7 @@ data: instance_type: obtenant rule_name: tenant_log_disk_used_percent rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} on observer {{ $labels.svr_ip }} log used percent {{ $value }} is over threshold.' summary: 'High log disk used percent detected for tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }}.' @@ -150,7 +150,7 @@ data: instance_type: obtenant rule_name: tenant_log_stream_downgrade rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} downgrade log stream count is {{ $value }}.' summary: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} found log stream downgrade.' @@ -161,7 +161,7 @@ data: instance_type: obtenant rule_name: tenant_no_compaction rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} has not compact for {{ $value }} seconds.' summary: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} has not compact for a long time.' @@ -172,7 +172,7 @@ data: instance_type: obtenant rule_name: tenant_no_frozen rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} has not frozen for {{ $value }} seconds.' summary: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} has not frozen for a long time.' @@ -183,7 +183,7 @@ data: instance_type: obtenant rule_name: tenant_partition_leader_absent rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} found {{ $value }} partition leader absent.' summary: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} found partition leader absent.' @@ -194,7 +194,7 @@ data: instance_type: obtenant rule_name: tenant_partition_replica_absent rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} found {{ $value }} partition replica absent.' summary: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} found partition replica absent.' @@ -205,7 +205,7 @@ data: instance_type: obtenant rule_name: tenant_task_timeout rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} found task not finished for {{ $value }} seconds.' summary: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} found task not finished for a long time.' @@ -216,7 +216,7 @@ data: instance_type: obtenant rule_name: standby_tenant_sync_delay rule_type: builtin - serverity: caution + severity: caution annotations: description: 'Standby tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} sync delay {{ $value }} seconds.' summary: 'Standby tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} sync delay too long.' @@ -227,7 +227,7 @@ data: instance_type: obtenant rule_name: standby_tenant_sync_error rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Standby tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} sync error.' summary: 'Standby tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} sync error.' @@ -238,7 +238,7 @@ data: instance_type: obtenant rule_name: tenant_memstore_percent rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} on observer {{ $labels.svr_ip }} memstore percent {{ $value }} is over threshold.' summary: 'High memstore used ratio detected for tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }}.' @@ -249,7 +249,7 @@ data: instance_type: obtenant rule_name: tenant_active_memstore_percent rule_type: builtin - serverity: warning + severity: warning annotations: description: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} on observer {{ $labels.svr_ip }} active memstore percent {{ $value }} is over threshold.' summary: 'High active memstore used ratio detected for tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }}.' @@ -260,7 +260,7 @@ data: instance_type: obtenant rule_name: tenant_thread_used_percent rule_type: builtin - serverity: caution + severity: caution annotations: description: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} thread used percent is {{ $value }}.' - summary: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} used too much thread.' + summary: 'Tenant {{ $labels.tenant_name }} of obcluster {{ $labels.ob_cluster_name }} used too much thread.'`}} diff --git a/charts/oceanbase-dashboard/values.yaml b/charts/oceanbase-dashboard/values.yaml index d7389b31d..4b3ba82c6 100644 --- a/charts/oceanbase-dashboard/values.yaml +++ b/charts/oceanbase-dashboard/values.yaml @@ -4,7 +4,8 @@ adminPassword: userCredentials: userNamespace: +configNamespace: service: type: NodePort - port: 80 \ No newline at end of file + port: 80 diff --git a/internal/dashboard/business/alarm/route.go b/internal/dashboard/business/alarm/route.go index 016673e4d..ddbb70026 100644 --- a/internal/dashboard/business/alarm/route.go +++ b/internal/dashboard/business/alarm/route.go @@ -73,14 +73,14 @@ func DeleteRoute(ctx context.Context, id string) error { return updateAlertManagerConfig(ctx, config) } -func CreateOrUpdateRoute(ctx context.Context, r *route.Route) error { +func CreateOrUpdateRoute(ctx context.Context, r *route.RouteParam) error { config, err := getAlertmanagerConfig(ctx) if err != nil { return errors.Wrap(err, errors.ErrExternal, "Failed to get config") } configRoutes := make([]*amconfig.Route, 0) for _, amroute := range config.Route.Routes { - if route.NewRoute(amroute).Hash() == r.Hash() { + if route.NewRoute(amroute).Hash() == r.Id { continue } configRoutes = append(configRoutes, amroute) diff --git a/internal/dashboard/generated/bindata/bindata.go b/internal/dashboard/generated/bindata/bindata.go index d54d96801..6087893b1 100644 --- a/internal/dashboard/generated/bindata/bindata.go +++ b/internal/dashboard/generated/bindata/bindata.go @@ -107,7 +107,7 @@ func internalAssetsMetric_en_usYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/metric_en_US.yaml", size: 27520, mode: os.FileMode(420), modTime: time.Unix(1715831084, 0)} + info := bindataFileInfo{name: "internal/assets/metric_en_US.yaml", size: 27520, mode: os.FileMode(420), modTime: time.Unix(1717678285, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -127,7 +127,7 @@ func internalAssetsMetric_exprYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/metric_expr.yaml", size: 37653, mode: os.FileMode(420), modTime: time.Unix(1715831084, 0)} + info := bindataFileInfo{name: "internal/assets/metric_expr.yaml", size: 37653, mode: os.FileMode(420), modTime: time.Unix(1717678285, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -147,7 +147,7 @@ func internalAssetsMetric_zh_cnYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/metric_zh_CN.yaml", size: 31146, mode: os.FileMode(420), modTime: time.Unix(1715831084, 0)} + info := bindataFileInfo{name: "internal/assets/metric_zh_CN.yaml", size: 31146, mode: os.FileMode(420), modTime: time.Unix(1717678285, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -167,7 +167,7 @@ func internalAssetsReceiver_templatesDiscord_configYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/receiver_templates/discord_config.yaml", size: 228, mode: os.FileMode(420), modTime: time.Unix(1717646434, 0)} + info := bindataFileInfo{name: "internal/assets/receiver_templates/discord_config.yaml", size: 228, mode: os.FileMode(420), modTime: time.Unix(1717678285, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -187,7 +187,7 @@ func internalAssetsReceiver_templatesEmail_configYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/receiver_templates/email_config.yaml", size: 351, mode: os.FileMode(420), modTime: time.Unix(1717646434, 0)} + info := bindataFileInfo{name: "internal/assets/receiver_templates/email_config.yaml", size: 351, mode: os.FileMode(420), modTime: time.Unix(1717678285, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -207,7 +207,7 @@ func internalAssetsReceiver_templatesMsteams_configYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/receiver_templates/msteams_config.yaml", size: 197, mode: os.FileMode(420), modTime: time.Unix(1717646434, 0)} + info := bindataFileInfo{name: "internal/assets/receiver_templates/msteams_config.yaml", size: 197, mode: os.FileMode(420), modTime: time.Unix(1717678285, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -227,7 +227,7 @@ func internalAssetsReceiver_templatesOpsgenie_configYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/receiver_templates/opsgenie_config.yaml", size: 774, mode: os.FileMode(420), modTime: time.Unix(1717646434, 0)} + info := bindataFileInfo{name: "internal/assets/receiver_templates/opsgenie_config.yaml", size: 774, mode: os.FileMode(420), modTime: time.Unix(1717678285, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -247,7 +247,7 @@ func internalAssetsReceiver_templatesPagerduty_configYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/receiver_templates/pagerduty_config.yaml", size: 1065, mode: os.FileMode(420), modTime: time.Unix(1717646434, 0)} + info := bindataFileInfo{name: "internal/assets/receiver_templates/pagerduty_config.yaml", size: 1065, mode: os.FileMode(420), modTime: time.Unix(1717678285, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -267,7 +267,7 @@ func internalAssetsReceiver_templatesPushover_configYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/receiver_templates/pushover_config.yaml", size: 940, mode: os.FileMode(420), modTime: time.Unix(1717646434, 0)} + info := bindataFileInfo{name: "internal/assets/receiver_templates/pushover_config.yaml", size: 940, mode: os.FileMode(420), modTime: time.Unix(1717678285, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -287,7 +287,7 @@ func internalAssetsReceiver_templatesSlack_configYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/receiver_templates/slack_config.yaml", size: 254, mode: os.FileMode(420), modTime: time.Unix(1717646434, 0)} + info := bindataFileInfo{name: "internal/assets/receiver_templates/slack_config.yaml", size: 254, mode: os.FileMode(420), modTime: time.Unix(1717678285, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -307,7 +307,7 @@ func internalAssetsReceiver_templatesSns_configYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/receiver_templates/sns_config.yaml", size: 477, mode: os.FileMode(420), modTime: time.Unix(1717646434, 0)} + info := bindataFileInfo{name: "internal/assets/receiver_templates/sns_config.yaml", size: 477, mode: os.FileMode(420), modTime: time.Unix(1717678285, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -327,7 +327,7 @@ func internalAssetsReceiver_templatesTelegram_configYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/receiver_templates/telegram_config.yaml", size: 441, mode: os.FileMode(420), modTime: time.Unix(1717646434, 0)} + info := bindataFileInfo{name: "internal/assets/receiver_templates/telegram_config.yaml", size: 441, mode: os.FileMode(420), modTime: time.Unix(1717678285, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -347,7 +347,7 @@ func internalAssetsReceiver_templatesVictorops_configYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/receiver_templates/victorops_config.yaml", size: 534, mode: os.FileMode(420), modTime: time.Unix(1717646434, 0)} + info := bindataFileInfo{name: "internal/assets/receiver_templates/victorops_config.yaml", size: 534, mode: os.FileMode(420), modTime: time.Unix(1717678285, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -367,7 +367,7 @@ func internalAssetsReceiver_templatesWebex_configYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/receiver_templates/webex_config.yaml", size: 313, mode: os.FileMode(420), modTime: time.Unix(1717646434, 0)} + info := bindataFileInfo{name: "internal/assets/receiver_templates/webex_config.yaml", size: 313, mode: os.FileMode(420), modTime: time.Unix(1717678285, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -387,7 +387,7 @@ func internalAssetsReceiver_templatesWebhook_configYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/receiver_templates/webhook_config.yaml", size: 183, mode: os.FileMode(420), modTime: time.Unix(1717646434, 0)} + info := bindataFileInfo{name: "internal/assets/receiver_templates/webhook_config.yaml", size: 183, mode: os.FileMode(420), modTime: time.Unix(1717678285, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -407,7 +407,7 @@ func internalAssetsReceiver_templatesWechat_configYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/receiver_templates/wechat_config.yaml", size: 491, mode: os.FileMode(420), modTime: time.Unix(1717646434, 0)} + info := bindataFileInfo{name: "internal/assets/receiver_templates/wechat_config.yaml", size: 491, mode: os.FileMode(420), modTime: time.Unix(1717678285, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -486,13 +486,11 @@ var _bindata = map[string]func() (*asset, error){ // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: -// -// data/ -// foo.txt -// img/ -// a.png -// b.png -// +// data/ +// foo.txt +// img/ +// a.png +// b.png // then AssetDir("data") would return []string{"foo.txt", "img"} // AssetDir("data/img") would return []string{"a.png", "b.png"} // AssetDir("foo.txt") and AssetDir("notexist") would return an error diff --git a/internal/dashboard/handler/alarm_handler.go b/internal/dashboard/handler/alarm_handler.go index 329a46fa2..99afae9e8 100644 --- a/internal/dashboard/handler/alarm_handler.go +++ b/internal/dashboard/handler/alarm_handler.go @@ -353,7 +353,7 @@ func GetRoute(ctx *gin.Context) (*route.RouteResponse, error) { // @Description Create or update alarm route. // @Accept application/json // @Produce application/json -// @Param body body route.Route true "route" +// @Param body body route.RouteParam true "route" // @Success 200 object response.APIResponse{data=route.RouteResponse} // @Failure 400 object response.APIResponse // @Failure 401 object response.APIResponse @@ -361,7 +361,7 @@ func GetRoute(ctx *gin.Context) (*route.RouteResponse, error) { // @Router /api/v1/alarm/route/routes [PUT] // @Security ApiKeyAuth func CreateOrUpdateRoute(ctx *gin.Context) (*route.RouteResponse, error) { - route := &route.Route{} + route := &route.RouteParam{} err := ctx.Bind(route) if err != nil { return nil, httpErr.NewBadRequest(err.Error()) diff --git a/internal/dashboard/model/alarm/route/route.go b/internal/dashboard/model/alarm/route/route.go index 3c082ba5e..37ce08ac4 100644 --- a/internal/dashboard/model/alarm/route/route.go +++ b/internal/dashboard/model/alarm/route/route.go @@ -45,6 +45,11 @@ type RouteResponse struct { Route } +type RouteParam struct { + Id string `json:"id,omitempty"` + Route +} + func (r *Route) Hash() string { routeBytes, err := json.Marshal(r) if err != nil {