github.com/netdata/go.d.plugin@v0.58.1/modules/coredns/collect.go (about) 1 // SPDX-License-Identifier: GPL-3.0-or-later 2 3 package coredns 4 5 import ( 6 "errors" 7 "fmt" 8 "strings" 9 10 "github.com/blang/semver/v4" 11 "github.com/netdata/go.d.plugin/pkg/prometheus" 12 "github.com/netdata/go.d.plugin/pkg/stm" 13 ) 14 15 const ( 16 metricPanicCountTotal169orOlder = "coredns_panic_count_total" 17 metricRequestCountTotal169orOlder = "coredns_dns_request_count_total" 18 metricRequestTypeCountTotal169orOlder = "coredns_dns_request_type_count_total" 19 metricResponseRcodeCountTotal169orOlder = "coredns_dns_response_rcode_count_total" 20 21 metricPanicCountTotal170orNewer = "coredns_panics_total" 22 metricRequestCountTotal170orNewer = "coredns_dns_requests_total" 23 metricRequestTypeCountTotal170orNewer = "coredns_dns_requests_total" 24 metricResponseRcodeCountTotal170orNewer = "coredns_dns_responses_total" 25 ) 26 27 var ( 28 empty = "" 29 dropped = "dropped" 30 emptyServerReplaceName = "empty" 31 rootZoneReplaceName = "root" 32 version169 = semver.MustParse("1.6.9") 33 ) 34 35 type requestMetricsNames struct { 36 panicCountTotal string 37 // true for all metrics below: 38 // - if none of server block matches 'server' tag is "", empty server has only one zone - dropped. 39 // example: 40 // coredns_dns_requests_total{family="1",proto="udp",server="",zone="dropped"} 1 for 41 // - dropped requests are added to both dropped and corresponding zone 42 // example: 43 // coredns_dns_requests_total{family="1",proto="udp",server="dns://:53",zone="dropped"} 2 44 // coredns_dns_requests_total{family="1",proto="udp",server="dns://:53",zone="ya.ru."} 2 45 requestCountTotal string 46 requestTypeCountTotal string 47 responseRcodeCountTotal string 48 } 49 50 func (cd *CoreDNS) collect() (map[string]int64, error) { 51 raw, err := cd.prom.ScrapeSeries() 52 53 if err != nil { 54 return nil, err 55 } 56 57 mx := newMetrics() 58 59 // some metric names are different depending on the version 60 // update them once 61 if !cd.skipVersionCheck { 62 cd.updateVersionDependentMetrics(raw) 63 cd.skipVersionCheck = true 64 } 65 66 //we can only get these metrics if we know the server version 67 if cd.version == nil { 68 return nil, errors.New("unable to determine server version") 69 } 70 71 cd.collectPanic(mx, raw) 72 cd.collectSummaryRequests(mx, raw) 73 cd.collectSummaryRequestsPerType(mx, raw) 74 cd.collectSummaryResponsesPerRcode(mx, raw) 75 76 if cd.perServerMatcher != nil { 77 cd.collectPerServerRequests(mx, raw) 78 //cd.collectPerServerRequestsDuration(mx, raw) 79 cd.collectPerServerRequestPerType(mx, raw) 80 cd.collectPerServerResponsePerRcode(mx, raw) 81 } 82 83 if cd.perZoneMatcher != nil { 84 cd.collectPerZoneRequests(mx, raw) 85 //cd.collectPerZoneRequestsDuration(mx, raw) 86 cd.collectPerZoneRequestsPerType(mx, raw) 87 cd.collectPerZoneResponsesPerRcode(mx, raw) 88 } 89 90 return stm.ToMap(mx), nil 91 } 92 93 func (cd *CoreDNS) updateVersionDependentMetrics(raw prometheus.Series) { 94 version := cd.parseVersion(raw) 95 if version == nil { 96 return 97 } 98 cd.version = version 99 if cd.version.LTE(version169) { 100 cd.metricNames.panicCountTotal = metricPanicCountTotal169orOlder 101 cd.metricNames.requestCountTotal = metricRequestCountTotal169orOlder 102 cd.metricNames.requestTypeCountTotal = metricRequestTypeCountTotal169orOlder 103 cd.metricNames.responseRcodeCountTotal = metricResponseRcodeCountTotal169orOlder 104 } else { 105 cd.metricNames.panicCountTotal = metricPanicCountTotal170orNewer 106 cd.metricNames.requestCountTotal = metricRequestCountTotal170orNewer 107 cd.metricNames.requestTypeCountTotal = metricRequestTypeCountTotal170orNewer 108 cd.metricNames.responseRcodeCountTotal = metricResponseRcodeCountTotal170orNewer 109 } 110 } 111 112 func (cd *CoreDNS) parseVersion(raw prometheus.Series) *semver.Version { 113 var versionStr string 114 for _, metric := range raw.FindByName("coredns_build_info") { 115 versionStr = metric.Labels.Get("version") 116 } 117 if versionStr == "" { 118 cd.Error("cannot find version string in metrics") 119 return nil 120 } 121 122 version, err := semver.Make(versionStr) 123 if err != nil { 124 cd.Errorf("failed to find server version: %v", err) 125 return nil 126 } 127 return &version 128 } 129 130 func (cd *CoreDNS) collectPanic(mx *metrics, raw prometheus.Series) { 131 mx.Panic.Set(raw.FindByName(cd.metricNames.panicCountTotal).Max()) 132 } 133 134 func (cd *CoreDNS) collectSummaryRequests(mx *metrics, raw prometheus.Series) { 135 for _, metric := range raw.FindByName(cd.metricNames.requestCountTotal) { 136 var ( 137 family = metric.Labels.Get("family") 138 proto = metric.Labels.Get("proto") 139 server = metric.Labels.Get("server") 140 zone = metric.Labels.Get("zone") 141 value = metric.Value 142 ) 143 144 if family == empty || proto == empty || zone == empty { 145 continue 146 } 147 148 if server == empty { 149 mx.NoZoneDropped.Add(value) 150 } 151 152 setRequestPerStatus(&mx.Summary.Request, value, server, zone) 153 154 if zone == dropped && server != empty { 155 continue 156 } 157 158 mx.Summary.Request.Total.Add(value) 159 setRequestPerIPFamily(&mx.Summary.Request, value, family) 160 setRequestPerProto(&mx.Summary.Request, value, proto) 161 } 162 } 163 164 //func (cd *CoreDNS) collectSummaryRequestsDuration(mx *metrics, raw prometheus.Series) { 165 // for _, metric := range raw.FindByName(metricRequestDurationSecondsBucket) { 166 // var ( 167 // server = metric.Labels.Get("server") 168 // zone = metric.Labels.Get("zone") 169 // le = metric.Labels.Get("le") 170 // value = metric.Value 171 // ) 172 // 173 // if zone == empty || zone == dropped && server != empty || le == empty { 174 // continue 175 // } 176 // 177 // setRequestDuration(&mx.Summary.Request, value, le) 178 // } 179 // processRequestDuration(&mx.Summary.Request) 180 //} 181 182 func (cd *CoreDNS) collectSummaryRequestsPerType(mx *metrics, raw prometheus.Series) { 183 for _, metric := range raw.FindByName(cd.metricNames.requestTypeCountTotal) { 184 var ( 185 server = metric.Labels.Get("server") 186 typ = metric.Labels.Get("type") 187 zone = metric.Labels.Get("zone") 188 value = metric.Value 189 ) 190 191 if typ == empty || zone == empty || zone == dropped && server != empty { 192 continue 193 } 194 195 setRequestPerType(&mx.Summary.Request, value, typ) 196 } 197 } 198 199 func (cd *CoreDNS) collectSummaryResponsesPerRcode(mx *metrics, raw prometheus.Series) { 200 for _, metric := range raw.FindByName(cd.metricNames.responseRcodeCountTotal) { 201 var ( 202 rcode = metric.Labels.Get("rcode") 203 server = metric.Labels.Get("server") 204 zone = metric.Labels.Get("zone") 205 value = metric.Value 206 ) 207 208 if rcode == empty || zone == empty || zone == dropped && server != empty { 209 continue 210 } 211 212 setResponsePerRcode(&mx.Summary.Response, value, rcode) 213 } 214 } 215 216 // Per Server 217 218 func (cd *CoreDNS) collectPerServerRequests(mx *metrics, raw prometheus.Series) { 219 for _, metric := range raw.FindByName(cd.metricNames.requestCountTotal) { 220 var ( 221 family = metric.Labels.Get("family") 222 proto = metric.Labels.Get("proto") 223 server = metric.Labels.Get("server") 224 zone = metric.Labels.Get("zone") 225 value = metric.Value 226 ) 227 228 if family == empty || proto == empty || zone == empty { 229 continue 230 } 231 232 if !cd.perServerMatcher.MatchString(server) { 233 continue 234 } 235 236 if server == empty { 237 server = emptyServerReplaceName 238 } 239 240 if !cd.collectedServers[server] { 241 cd.addNewServerCharts(server) 242 cd.collectedServers[server] = true 243 } 244 245 if _, ok := mx.PerServer[server]; !ok { 246 mx.PerServer[server] = &requestResponse{} 247 } 248 249 srv := mx.PerServer[server] 250 251 setRequestPerStatus(&srv.Request, value, server, zone) 252 253 if zone == dropped && server != emptyServerReplaceName { 254 continue 255 } 256 257 srv.Request.Total.Add(value) 258 setRequestPerIPFamily(&srv.Request, value, family) 259 setRequestPerProto(&srv.Request, value, proto) 260 } 261 } 262 263 //func (cd *CoreDNS) collectPerServerRequestsDuration(mx *metrics, raw prometheus.Series) { 264 // for _, metric := range raw.FindByName(metricRequestDurationSecondsBucket) { 265 // var ( 266 // server = metric.Labels.Get("server") 267 // zone = metric.Labels.Get("zone") 268 // le = metric.Labels.Get("le") 269 // value = metric.Value 270 // ) 271 // 272 // if zone == empty || zone == dropped && server != empty || le == empty { 273 // continue 274 // } 275 // 276 // if !cd.perServerMatcher.MatchString(server) { 277 // continue 278 // } 279 // 280 // if server == empty { 281 // server = emptyServerReplaceName 282 // } 283 // 284 // if !cd.collectedServers[server] { 285 // cd.addNewServerCharts(server) 286 // cd.collectedServers[server] = true 287 // } 288 // 289 // if _, ok := mx.PerServer[server]; !ok { 290 // mx.PerServer[server] = &requestResponse{} 291 // } 292 // 293 // setRequestDuration(&mx.PerServer[server].Request, value, le) 294 // } 295 // for _, s := range mx.PerServer { 296 // processRequestDuration(&s.Request) 297 // } 298 //} 299 300 func (cd *CoreDNS) collectPerServerRequestPerType(mx *metrics, raw prometheus.Series) { 301 for _, metric := range raw.FindByName(cd.metricNames.requestTypeCountTotal) { 302 var ( 303 server = metric.Labels.Get("server") 304 typ = metric.Labels.Get("type") 305 zone = metric.Labels.Get("zone") 306 value = metric.Value 307 ) 308 309 if typ == empty || zone == empty || zone == dropped && server != empty { 310 continue 311 } 312 313 if !cd.perServerMatcher.MatchString(server) { 314 continue 315 } 316 317 if server == empty { 318 server = emptyServerReplaceName 319 } 320 321 if !cd.collectedServers[server] { 322 cd.addNewServerCharts(server) 323 cd.collectedServers[server] = true 324 } 325 326 if _, ok := mx.PerServer[server]; !ok { 327 mx.PerServer[server] = &requestResponse{} 328 } 329 330 setRequestPerType(&mx.PerServer[server].Request, value, typ) 331 } 332 } 333 334 func (cd *CoreDNS) collectPerServerResponsePerRcode(mx *metrics, raw prometheus.Series) { 335 for _, metric := range raw.FindByName(cd.metricNames.responseRcodeCountTotal) { 336 var ( 337 rcode = metric.Labels.Get("rcode") 338 server = metric.Labels.Get("server") 339 zone = metric.Labels.Get("zone") 340 value = metric.Value 341 ) 342 343 if rcode == empty || zone == empty || zone == dropped && server != empty { 344 continue 345 } 346 347 if !cd.perServerMatcher.MatchString(server) { 348 continue 349 } 350 351 if server == empty { 352 server = emptyServerReplaceName 353 } 354 355 if !cd.collectedServers[server] { 356 cd.addNewServerCharts(server) 357 cd.collectedServers[server] = true 358 } 359 360 if _, ok := mx.PerServer[server]; !ok { 361 mx.PerServer[server] = &requestResponse{} 362 } 363 364 setResponsePerRcode(&mx.PerServer[server].Response, value, rcode) 365 } 366 } 367 368 // Per Zone 369 370 func (cd *CoreDNS) collectPerZoneRequests(mx *metrics, raw prometheus.Series) { 371 for _, metric := range raw.FindByName(cd.metricNames.requestCountTotal) { 372 var ( 373 family = metric.Labels.Get("family") 374 proto = metric.Labels.Get("proto") 375 zone = metric.Labels.Get("zone") 376 value = metric.Value 377 ) 378 379 if family == empty || proto == empty || zone == empty { 380 continue 381 } 382 383 if !cd.perZoneMatcher.MatchString(zone) { 384 continue 385 } 386 387 if zone == "." { 388 zone = rootZoneReplaceName 389 } 390 391 if !cd.collectedZones[zone] { 392 cd.addNewZoneCharts(zone) 393 cd.collectedZones[zone] = true 394 } 395 396 if _, ok := mx.PerZone[zone]; !ok { 397 mx.PerZone[zone] = &requestResponse{} 398 } 399 400 zoneMX := mx.PerZone[zone] 401 zoneMX.Request.Total.Add(value) 402 setRequestPerIPFamily(&zoneMX.Request, value, family) 403 setRequestPerProto(&zoneMX.Request, value, proto) 404 } 405 } 406 407 //func (cd *CoreDNS) collectPerZoneRequestsDuration(mx *metrics, raw prometheus.Series) { 408 // for _, metric := range raw.FindByName(metricRequestDurationSecondsBucket) { 409 // var ( 410 // zone = metric.Labels.Get("zone") 411 // le = metric.Labels.Get("le") 412 // value = metric.Value 413 // ) 414 // 415 // if zone == empty || le == empty { 416 // continue 417 // } 418 // 419 // if !cd.perZoneMatcher.MatchString(zone) { 420 // continue 421 // } 422 // 423 // if zone == "." { 424 // zone = rootZoneReplaceName 425 // } 426 // 427 // if !cd.collectedZones[zone] { 428 // cd.addNewZoneCharts(zone) 429 // cd.collectedZones[zone] = true 430 // } 431 // 432 // if _, ok := mx.PerZone[zone]; !ok { 433 // mx.PerZone[zone] = &requestResponse{} 434 // } 435 // 436 // setRequestDuration(&mx.PerZone[zone].Request, value, le) 437 // } 438 // for _, s := range mx.PerZone { 439 // processRequestDuration(&s.Request) 440 // } 441 //} 442 443 func (cd *CoreDNS) collectPerZoneRequestsPerType(mx *metrics, raw prometheus.Series) { 444 for _, metric := range raw.FindByName(cd.metricNames.requestTypeCountTotal) { 445 var ( 446 typ = metric.Labels.Get("type") 447 zone = metric.Labels.Get("zone") 448 value = metric.Value 449 ) 450 451 if typ == empty || zone == empty { 452 continue 453 } 454 455 if !cd.perZoneMatcher.MatchString(zone) { 456 continue 457 } 458 459 if zone == "." { 460 zone = rootZoneReplaceName 461 } 462 463 if !cd.collectedZones[zone] { 464 cd.addNewZoneCharts(zone) 465 cd.collectedZones[zone] = true 466 } 467 468 if _, ok := mx.PerZone[zone]; !ok { 469 mx.PerZone[zone] = &requestResponse{} 470 } 471 472 setRequestPerType(&mx.PerZone[zone].Request, value, typ) 473 } 474 } 475 476 func (cd *CoreDNS) collectPerZoneResponsesPerRcode(mx *metrics, raw prometheus.Series) { 477 for _, metric := range raw.FindByName(cd.metricNames.responseRcodeCountTotal) { 478 var ( 479 rcode = metric.Labels.Get("rcode") 480 zone = metric.Labels.Get("zone") 481 value = metric.Value 482 ) 483 484 if rcode == empty || zone == empty { 485 continue 486 } 487 488 if !cd.perZoneMatcher.MatchString(zone) { 489 continue 490 } 491 492 if zone == "." { 493 zone = rootZoneReplaceName 494 } 495 496 if !cd.collectedZones[zone] { 497 cd.addNewZoneCharts(zone) 498 cd.collectedZones[zone] = true 499 } 500 501 if _, ok := mx.PerZone[zone]; !ok { 502 mx.PerZone[zone] = &requestResponse{} 503 } 504 505 setResponsePerRcode(&mx.PerZone[zone].Response, value, rcode) 506 } 507 } 508 509 // --- 510 511 func setRequestPerIPFamily(mx *request, value float64, family string) { 512 switch family { 513 case "1": 514 mx.PerIPFamily.IPv4.Add(value) 515 case "2": 516 mx.PerIPFamily.IPv6.Add(value) 517 } 518 } 519 520 func setRequestPerProto(mx *request, value float64, proto string) { 521 switch proto { 522 case "udp": 523 mx.PerProto.UDP.Add(value) 524 case "tcp": 525 mx.PerProto.TCP.Add(value) 526 } 527 } 528 529 func setRequestPerStatus(mx *request, value float64, server, zone string) { 530 switch zone { 531 default: 532 mx.PerStatus.Processed.Add(value) 533 case "dropped": 534 mx.PerStatus.Dropped.Add(value) 535 if server == empty || server == emptyServerReplaceName { 536 return 537 } 538 mx.PerStatus.Processed.Sub(value) 539 } 540 } 541 542 func setRequestPerType(mx *request, value float64, typ string) { 543 switch typ { 544 default: 545 mx.PerType.Other.Add(value) 546 case "A": 547 mx.PerType.A.Add(value) 548 case "AAAA": 549 mx.PerType.AAAA.Add(value) 550 case "MX": 551 mx.PerType.MX.Add(value) 552 case "SOA": 553 mx.PerType.SOA.Add(value) 554 case "CNAME": 555 mx.PerType.CNAME.Add(value) 556 case "PTR": 557 mx.PerType.PTR.Add(value) 558 case "TXT": 559 mx.PerType.TXT.Add(value) 560 case "NS": 561 mx.PerType.NS.Add(value) 562 case "DS": 563 mx.PerType.DS.Add(value) 564 case "DNSKEY": 565 mx.PerType.DNSKEY.Add(value) 566 case "RRSIG": 567 mx.PerType.RRSIG.Add(value) 568 case "NSEC": 569 mx.PerType.NSEC.Add(value) 570 case "NSEC3": 571 mx.PerType.NSEC3.Add(value) 572 case "IXFR": 573 mx.PerType.IXFR.Add(value) 574 case "ANY": 575 mx.PerType.ANY.Add(value) 576 } 577 } 578 579 func setResponsePerRcode(mx *response, value float64, rcode string) { 580 mx.Total.Add(value) 581 582 switch rcode { 583 default: 584 mx.PerRcode.Other.Add(value) 585 case "NOERROR": 586 mx.PerRcode.NOERROR.Add(value) 587 case "FORMERR": 588 mx.PerRcode.FORMERR.Add(value) 589 case "SERVFAIL": 590 mx.PerRcode.SERVFAIL.Add(value) 591 case "NXDOMAIN": 592 mx.PerRcode.NXDOMAIN.Add(value) 593 case "NOTIMP": 594 mx.PerRcode.NOTIMP.Add(value) 595 case "REFUSED": 596 mx.PerRcode.REFUSED.Add(value) 597 case "YXDOMAIN": 598 mx.PerRcode.YXDOMAIN.Add(value) 599 case "YXRRSET": 600 mx.PerRcode.YXRRSET.Add(value) 601 case "NXRRSET": 602 mx.PerRcode.NXRRSET.Add(value) 603 case "NOTAUTH": 604 mx.PerRcode.NOTAUTH.Add(value) 605 case "NOTZONE": 606 mx.PerRcode.NOTZONE.Add(value) 607 case "BADSIG": 608 mx.PerRcode.BADSIG.Add(value) 609 case "BADKEY": 610 mx.PerRcode.BADKEY.Add(value) 611 case "BADTIME": 612 mx.PerRcode.BADTIME.Add(value) 613 case "BADMODE": 614 mx.PerRcode.BADMODE.Add(value) 615 case "BADNAME": 616 mx.PerRcode.BADNAME.Add(value) 617 case "BADALG": 618 mx.PerRcode.BADALG.Add(value) 619 case "BADTRUNC": 620 mx.PerRcode.BADTRUNC.Add(value) 621 case "BADCOOKIE": 622 mx.PerRcode.BADCOOKIE.Add(value) 623 } 624 } 625 626 //func setRequestDuration(mx *request, value float64, le string) { 627 // switch le { 628 // case "0.00025": 629 // mx.Duration.LE000025.Add(value) 630 // case "0.0005": 631 // mx.Duration.LE00005.Add(value) 632 // case "0.001": 633 // mx.Duration.LE0001.Add(value) 634 // case "0.002": 635 // mx.Duration.LE0002.Add(value) 636 // case "0.004": 637 // mx.Duration.LE0004.Add(value) 638 // case "0.008": 639 // mx.Duration.LE0008.Add(value) 640 // case "0.016": 641 // mx.Duration.LE0016.Add(value) 642 // case "0.032": 643 // mx.Duration.LE0032.Add(value) 644 // case "0.064": 645 // mx.Duration.LE0064.Add(value) 646 // case "0.128": 647 // mx.Duration.LE0128.Add(value) 648 // case "0.256": 649 // mx.Duration.LE0256.Add(value) 650 // case "0.512": 651 // mx.Duration.LE0512.Add(value) 652 // case "1.024": 653 // mx.Duration.LE1024.Add(value) 654 // case "2.048": 655 // mx.Duration.LE2048.Add(value) 656 // case "4.096": 657 // mx.Duration.LE4096.Add(value) 658 // case "8.192": 659 // mx.Duration.LE8192.Add(value) 660 // case "+Inf": 661 // mx.Duration.LEInf.Add(value) 662 // } 663 //} 664 665 //func processRequestDuration(mx *request) { 666 // mx.Duration.LEInf.Sub(mx.Duration.LE8192.Value()) 667 // mx.Duration.LE8192.Sub(mx.Duration.LE4096.Value()) 668 // mx.Duration.LE4096.Sub(mx.Duration.LE2048.Value()) 669 // mx.Duration.LE2048.Sub(mx.Duration.LE1024.Value()) 670 // mx.Duration.LE1024.Sub(mx.Duration.LE0512.Value()) 671 // mx.Duration.LE0512.Sub(mx.Duration.LE0256.Value()) 672 // mx.Duration.LE0256.Sub(mx.Duration.LE0128.Value()) 673 // mx.Duration.LE0128.Sub(mx.Duration.LE0064.Value()) 674 // mx.Duration.LE0064.Sub(mx.Duration.LE0032.Value()) 675 // mx.Duration.LE0032.Sub(mx.Duration.LE0016.Value()) 676 // mx.Duration.LE0016.Sub(mx.Duration.LE0008.Value()) 677 // mx.Duration.LE0008.Sub(mx.Duration.LE0004.Value()) 678 // mx.Duration.LE0004.Sub(mx.Duration.LE0002.Value()) 679 // mx.Duration.LE0002.Sub(mx.Duration.LE0001.Value()) 680 // mx.Duration.LE0001.Sub(mx.Duration.LE00005.Value()) 681 // mx.Duration.LE00005.Sub(mx.Duration.LE000025.Value()) 682 //} 683 684 // --- 685 686 func (cd *CoreDNS) addNewServerCharts(name string) { 687 charts := serverCharts.Copy() 688 for _, chart := range *charts { 689 chart.ID = fmt.Sprintf(chart.ID, "server", name) 690 chart.Title = fmt.Sprintf(chart.Title, "Server", name) 691 chart.Fam = fmt.Sprintf(chart.Fam, "server", name) 692 693 for _, dim := range chart.Dims { 694 dim.ID = fmt.Sprintf(dim.ID, name) 695 } 696 } 697 _ = cd.charts.Add(*charts...) 698 } 699 700 func (cd *CoreDNS) addNewZoneCharts(name string) { 701 charts := zoneCharts.Copy() 702 for _, chart := range *charts { 703 chart.ID = fmt.Sprintf(chart.ID, "zone", name) 704 chart.Title = fmt.Sprintf(chart.Title, "Zone", name) 705 chart.Fam = fmt.Sprintf(chart.Fam, "zone", name) 706 chart.Ctx = strings.Replace(chart.Ctx, "coredns.server_", "coredns.zone_", 1) 707 708 for _, dim := range chart.Dims { 709 dim.ID = fmt.Sprintf(dim.ID, name) 710 } 711 } 712 _ = cd.charts.Add(*charts...) 713 }