github.com/m3db/m3@v1.5.0/src/cmd/services/m3coordinator/ingest/carbon/ingest.go (about) 1 // Copyright (c) 2019 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 // Package ingestcarbon implements a carbon ingester. 22 package ingestcarbon 23 24 import ( 25 "bytes" 26 "context" 27 "errors" 28 "fmt" 29 "net" 30 "regexp" 31 "sort" 32 "sync" 33 "time" 34 35 "github.com/m3db/m3/src/cmd/services/m3coordinator/downsample" 36 "github.com/m3db/m3/src/cmd/services/m3coordinator/ingest" 37 "github.com/m3db/m3/src/cmd/services/m3query/config" 38 "github.com/m3db/m3/src/metrics/aggregation" 39 "github.com/m3db/m3/src/metrics/carbon" 40 "github.com/m3db/m3/src/metrics/policy" 41 "github.com/m3db/m3/src/query/graphite/graphite" 42 "github.com/m3db/m3/src/query/models" 43 "github.com/m3db/m3/src/query/storage/m3" 44 "github.com/m3db/m3/src/query/storage/m3/storagemetadata" 45 "github.com/m3db/m3/src/query/ts" 46 "github.com/m3db/m3/src/x/instrument" 47 "github.com/m3db/m3/src/x/pool" 48 m3xserver "github.com/m3db/m3/src/x/server" 49 xsync "github.com/m3db/m3/src/x/sync" 50 xtime "github.com/m3db/m3/src/x/time" 51 52 "github.com/uber-go/tally" 53 "go.uber.org/zap" 54 "go.uber.org/zap/zapcore" 55 ) 56 57 const ( 58 maxResourcePoolNameSize = 1024 59 maxPooledTagsSize = 16 60 defaultResourcePoolSize = 4096 61 ) 62 63 var ( 64 // Used for parsing carbon names into tags. 65 carbonSeparatorByte = byte('.') 66 carbonSeparatorBytes = []byte{carbonSeparatorByte} 67 68 errCannotGenerateTagsFromEmptyName = errors.New("cannot generate tags from empty name") 69 errIOptsMustBeSet = errors.New("carbon ingester options: instrument options must be st") 70 errWorkerPoolMustBeSet = errors.New("carbon ingester options: worker pool must be set") 71 ) 72 73 // Options configures the ingester. 74 type Options struct { 75 InstrumentOptions instrument.Options 76 WorkerPool xsync.PooledWorkerPool 77 IngesterConfig config.CarbonIngesterConfiguration 78 } 79 80 // CarbonIngesterRules contains the carbon ingestion rules. 81 type CarbonIngesterRules struct { 82 Rules []config.CarbonIngesterRuleConfiguration 83 } 84 85 // Validate validates the options struct. 86 func (o *Options) Validate() error { 87 if o.InstrumentOptions == nil { 88 return errIOptsMustBeSet 89 } 90 if o.WorkerPool == nil { 91 return errWorkerPoolMustBeSet 92 } 93 return nil 94 } 95 96 // NewIngester returns an ingester for carbon metrics. 97 func NewIngester( 98 downsamplerAndWriter ingest.DownsamplerAndWriter, 99 clusterNamespacesWatcher m3.ClusterNamespacesWatcher, 100 opts Options, 101 ) (m3xserver.Handler, error) { 102 err := opts.Validate() 103 if err != nil { 104 return nil, err 105 } 106 107 tagOpts := models.NewTagOptions().SetIDSchemeType(models.TypeGraphite) 108 err = tagOpts.Validate() 109 if err != nil { 110 return nil, err 111 } 112 113 poolOpts := pool.NewObjectPoolOptions(). 114 SetInstrumentOptions(opts.InstrumentOptions). 115 SetRefillLowWatermark(0). 116 SetRefillHighWatermark(0). 117 SetSize(defaultResourcePoolSize) 118 119 resourcePool := pool.NewObjectPool(poolOpts) 120 resourcePool.Init(func() interface{} { 121 return &lineResources{ 122 name: make([]byte, 0, maxResourcePoolNameSize), 123 datapoints: make([]ts.Datapoint, 1), 124 tags: make([]models.Tag, 0, maxPooledTagsSize), 125 } 126 }) 127 128 scope := opts.InstrumentOptions.MetricsScope() 129 metrics, err := newCarbonIngesterMetrics(scope) 130 if err != nil { 131 return nil, err 132 } 133 134 ingester := &ingester{ 135 downsamplerAndWriter: downsamplerAndWriter, 136 opts: opts, 137 logger: opts.InstrumentOptions.Logger(), 138 tagOpts: tagOpts, 139 metrics: metrics, 140 lineResourcesPool: resourcePool, 141 } 142 // No need to retain watch as NamespaceWatcher.Close() will handle closing any watches 143 // generated by creating listeners. 144 clusterNamespacesWatcher.RegisterListener(ingester) 145 146 return ingester, nil 147 } 148 149 type ingester struct { 150 downsamplerAndWriter ingest.DownsamplerAndWriter 151 opts Options 152 logger *zap.Logger 153 metrics carbonIngesterMetrics 154 tagOpts models.TagOptions 155 156 lineResourcesPool pool.ObjectPool 157 158 sync.RWMutex 159 rules []ruleAndMatcher 160 } 161 162 func (i *ingester) OnUpdate(clusterNamespaces m3.ClusterNamespaces) { 163 i.Lock() 164 defer i.Unlock() 165 166 rules := i.regenerateIngestionRulesWithLock(clusterNamespaces) 167 if rules == nil { 168 namespaces := make([]string, 0, len(clusterNamespaces)) 169 for _, ns := range clusterNamespaces { 170 namespaces = append(namespaces, ns.NamespaceID().String()) 171 } 172 i.logger.Warn("generated empty carbon ingestion rules from latest cluster namespaces update. leaving"+ 173 " current set of rules as-is.", zap.Strings("namespaces", namespaces)) 174 return 175 } 176 177 compiledRules, err := i.compileRulesWithLock(*rules) 178 if err != nil { 179 i.logger.Error("failed to compile latest rules. continuing to use existing carbon ingestion "+ 180 "rules", zap.Error(err)) 181 return 182 } 183 184 i.rules = compiledRules 185 } 186 187 func (i *ingester) regenerateIngestionRulesWithLock(clusterNamespaces m3.ClusterNamespaces) *CarbonIngesterRules { 188 var ( 189 rules = &CarbonIngesterRules{ 190 Rules: i.opts.IngesterConfig.RulesOrDefault(clusterNamespaces), 191 } 192 namespacesByRetention = make(map[m3.RetentionResolution]m3.ClusterNamespace, len(clusterNamespaces)) 193 ) 194 195 for _, ns := range clusterNamespaces { 196 if ns.Options().Attributes().MetricsType == storagemetadata.AggregatedMetricsType { 197 resRet := m3.RetentionResolution{ 198 Resolution: ns.Options().Attributes().Resolution, 199 Retention: ns.Options().Attributes().Retention, 200 } 201 // This should never happen 202 if _, ok := namespacesByRetention[resRet]; ok { 203 i.logger.Error( 204 "cannot have namespaces with duplicate resolution and retention", 205 zap.String("resolution", resRet.Resolution.String()), 206 zap.String("retention", resRet.Retention.String())) 207 return nil 208 } 209 210 namespacesByRetention[resRet] = ns 211 } 212 } 213 214 // Validate rule policies. 215 for _, rule := range rules.Rules { 216 // Sort so we can detect duplicates. 217 sort.Slice(rule.Policies, func(i, j int) bool { 218 if rule.Policies[i].Resolution == rule.Policies[j].Resolution { 219 return rule.Policies[i].Retention < rule.Policies[j].Retention 220 } 221 222 return rule.Policies[i].Resolution < rule.Policies[j].Resolution 223 }) 224 225 var lastPolicy config.CarbonIngesterStoragePolicyConfiguration 226 for idx, policy := range rule.Policies { 227 if policy == lastPolicy { 228 i.logger.Error( 229 "cannot include the same storage policy multiple times for a single carbon ingestion rule", 230 zap.String("pattern", rule.Pattern), 231 zap.String("resolution", policy.Resolution.String()), 232 zap.String("retention", policy.Retention.String())) 233 return nil 234 } 235 236 if idx > 0 && !rule.Aggregation.EnabledOrDefault() && policy.Resolution != lastPolicy.Resolution { 237 i.logger.Error( 238 "cannot include multiple storage policies with different resolutions if aggregation is disabled", 239 zap.String("pattern", rule.Pattern), 240 zap.String("resolution", policy.Resolution.String()), 241 zap.String("retention", policy.Retention.String())) 242 return nil 243 } 244 245 _, ok := namespacesByRetention[m3.RetentionResolution{ 246 Resolution: policy.Resolution, 247 Retention: policy.Retention, 248 }] 249 250 // Disallow storage policies that don't match any known M3DB clusters. 251 if !ok { 252 i.logger.Error( 253 "cannot enable carbon ingestion without a corresponding aggregated M3DB namespace", 254 zap.String("resolution", policy.Resolution.String()), zap.String("retention", policy.Retention.String())) 255 return nil 256 } 257 258 lastPolicy = policy 259 } 260 } 261 262 if len(rules.Rules) == 0 { 263 i.logger.Warn("no carbon ingestion rules were provided and no aggregated M3DB namespaces exist, carbon metrics will not be ingested") 264 return nil 265 } 266 267 if len(i.opts.IngesterConfig.Rules) == 0 { 268 i.logger.Info("no carbon ingestion rules were provided, all carbon metrics will be written to all aggregated M3DB namespaces") 269 } 270 271 return rules 272 } 273 274 func (i *ingester) Handle(conn net.Conn) { 275 var ( 276 // Interfaces require a context be passed, but M3DB client already has timeouts 277 // built in and allocating a new context each time is expensive so we just pass 278 // the same context always and rely on M3DB client timeouts. 279 ctx = context.Background() 280 wg = sync.WaitGroup{} 281 s = carbon.NewScanner(conn, i.opts.InstrumentOptions) 282 logger = i.opts.InstrumentOptions.Logger() 283 rewrite = &i.opts.IngesterConfig.Rewrite 284 ) 285 286 logger.Debug("handling new carbon ingestion connection") 287 for s.Scan() { 288 received := time.Now() 289 name, timestamp, value := s.Metric() 290 291 resources := i.getLineResources() 292 293 // Copy name since scanner bytes are recycled. 294 resources.name = copyAndRewrite(resources.name, name, rewrite) 295 296 wg.Add(1) 297 i.opts.WorkerPool.Go(func() { 298 ok := i.write(ctx, resources, xtime.ToUnixNano(timestamp), value) 299 if ok { 300 i.metrics.success.Inc(1) 301 } 302 303 now := time.Now() 304 305 // Always record age regardless of success/failure since 306 // sometimes errors can be due to how old the metrics are 307 // and not recording age would obscure this visibility from 308 // the metrics of how fresh/old the incoming metrics are. 309 age := now.Sub(timestamp) 310 i.metrics.ingestLatency.RecordDuration(age) 311 312 // Also record write latency (not relative to metric timestamp). 313 i.metrics.writeLatency.RecordDuration(now.Sub(received)) 314 315 // The contract is that after the DownsamplerAndWriter returns, any resources 316 // that it needed to hold onto have already been copied. 317 i.putLineResources(resources) 318 wg.Done() 319 }) 320 321 i.metrics.malformed.Inc(int64(s.MalformedCount)) 322 s.MalformedCount = 0 323 } 324 325 if err := s.Err(); err != nil { 326 logger.Error("encountered error during carbon ingestion when scanning connection", zap.Error(err)) 327 } 328 329 logger.Debug("waiting for outstanding carbon ingestion writes to complete") 330 wg.Wait() 331 logger.Debug("all outstanding writes completed, shutting down carbon ingestion handler") 332 333 // Don't close the connection, that is the server's responsibility. 334 } 335 336 func (i *ingester) write( 337 ctx context.Context, 338 resources *lineResources, 339 timestamp xtime.UnixNano, 340 value float64, 341 ) bool { 342 downsampleAndStoragePolicies := ingest.WriteOptions{ 343 // Set both of these overrides to true to indicate that only the exact mapping 344 // rules and storage policies that we provide should be used and that all 345 // default behavior (like performing all possible downsamplings and writing 346 // all data to the unaggregated namespace in storage) should be ignored. 347 DownsampleOverride: true, 348 WriteOverride: true, 349 } 350 351 matched := 0 352 defer func() { 353 if matched == 0 { 354 // No policies matched. 355 debugLog := i.logger.Check(zapcore.DebugLevel, "no rules matched carbon metric, skipping") 356 if debugLog != nil { 357 debugLog.Write(zap.ByteString("name", resources.name)) 358 } 359 return 360 } 361 362 debugLog := i.logger.Check(zapcore.DebugLevel, "successfully wrote carbon metric") 363 if debugLog != nil { 364 debugLog.Write(zap.ByteString("name", resources.name), 365 zap.Int("matchedRules", matched)) 366 } 367 }() 368 369 i.RLock() 370 rules := i.rules 371 i.RUnlock() 372 373 for _, rule := range rules { 374 var matches bool 375 switch { 376 case rule.rule.Pattern == graphite.MatchAllPattern: 377 matches = true 378 case rule.regexp != nil: 379 matches = rule.regexp.Match(resources.name) 380 case len(rule.contains) != 0: 381 matches = bytes.Contains(resources.name, rule.contains) 382 } 383 384 if matches { 385 // Each rule should only have either mapping rules or storage policies so 386 // one of these should be a no-op. 387 downsampleAndStoragePolicies.DownsampleMappingRules = rule.mappingRules 388 downsampleAndStoragePolicies.WriteStoragePolicies = rule.storagePolicies 389 390 debugLog := i.logger.Check(zapcore.DebugLevel, "carbon metric matched by pattern") 391 if debugLog != nil { 392 debugLog.Write(zap.ByteString("name", resources.name), 393 zap.String("pattern", rule.rule.Pattern), 394 zap.Any("mappingRules", rule.mappingRules), 395 zap.Any("storagePolicies", rule.storagePolicies)) 396 } 397 398 // Break because we only want to apply one rule per metric based on which 399 // ever one matches first. 400 err := i.writeWithOptions(ctx, resources, timestamp, value, downsampleAndStoragePolicies) 401 if err != nil { 402 return false 403 } 404 405 matched++ 406 407 // If continue is not specified then we matched the current set of rules. 408 if !rule.rule.Continue { 409 break 410 } 411 } 412 } 413 414 return matched > 0 415 } 416 417 func (i *ingester) writeWithOptions( 418 ctx context.Context, 419 resources *lineResources, 420 timestamp xtime.UnixNano, 421 value float64, 422 opts ingest.WriteOptions, 423 ) error { 424 resources.datapoints[0] = ts.Datapoint{Timestamp: timestamp, Value: value} 425 tags, err := GenerateTagsFromNameIntoSlice(resources.name, i.tagOpts, resources.tags) 426 if err != nil { 427 i.logger.Error("err generating tags from carbon", 428 zap.String("name", string(resources.name)), zap.Error(err)) 429 i.metrics.malformed.Inc(1) 430 return err 431 } 432 433 err = i.downsamplerAndWriter.Write(ctx, tags, resources.datapoints, 434 xtime.Second, nil, opts, ts.SourceTypeGraphite) 435 if err != nil { 436 i.logger.Error("err writing carbon metric", 437 zap.String("name", string(resources.name)), zap.Error(err)) 438 i.metrics.err.Inc(1) 439 return err 440 } 441 442 return nil 443 } 444 445 func (i *ingester) Close() { 446 // We don't maintain any state in-between connections so there is nothing to do here. 447 } 448 449 type carbonIngesterMetrics struct { 450 success tally.Counter 451 err tally.Counter 452 malformed tally.Counter 453 ingestLatency tally.Histogram 454 writeLatency tally.Histogram 455 } 456 457 func newCarbonIngesterMetrics(scope tally.Scope) (carbonIngesterMetrics, error) { 458 buckets, err := ingest.NewLatencyBuckets() 459 if err != nil { 460 return carbonIngesterMetrics{}, err 461 } 462 return carbonIngesterMetrics{ 463 success: scope.Counter("success"), 464 err: scope.Counter("error"), 465 malformed: scope.Counter("malformed"), 466 writeLatency: scope.SubScope("write").Histogram("latency", buckets.WriteLatencyBuckets), 467 ingestLatency: scope.SubScope("ingest").Histogram("latency", buckets.IngestLatencyBuckets), 468 }, nil 469 } 470 471 // GenerateTagsFromName accepts a carbon metric name and blows it up into a list of 472 // key-value pair tags such that an input like: 473 // foo.bar.baz 474 // becomes 475 // __g0__:foo 476 // __g1__:bar 477 // __g2__:baz 478 func GenerateTagsFromName( 479 name []byte, 480 opts models.TagOptions, 481 ) (models.Tags, error) { 482 return generateTagsFromName(name, opts, nil) 483 } 484 485 // GenerateTagsFromNameIntoSlice does the same thing as GenerateTagsFromName except 486 // it allows the caller to provide the slice into which the tags are appended. 487 func GenerateTagsFromNameIntoSlice( 488 name []byte, 489 opts models.TagOptions, 490 tags []models.Tag, 491 ) (models.Tags, error) { 492 return generateTagsFromName(name, opts, tags) 493 } 494 495 func generateTagsFromName( 496 name []byte, 497 opts models.TagOptions, 498 tags []models.Tag, 499 ) (models.Tags, error) { 500 if len(name) == 0 { 501 return models.EmptyTags(), errCannotGenerateTagsFromEmptyName 502 } 503 504 numTags := bytes.Count(name, carbonSeparatorBytes) + 1 505 506 if cap(tags) >= numTags { 507 tags = tags[:0] 508 } else { 509 tags = make([]models.Tag, 0, numTags) 510 } 511 512 startIdx := 0 513 tagNum := 0 514 for i, charByte := range name { 515 if charByte == carbonSeparatorByte { 516 if i+1 < len(name) && name[i+1] == carbonSeparatorByte { 517 return models.EmptyTags(), 518 fmt.Errorf("carbon metric: %s has duplicate separator", string(name)) 519 } 520 521 tags = append(tags, models.Tag{ 522 Name: graphite.TagName(tagNum), 523 Value: name[startIdx:i], 524 }) 525 startIdx = i + 1 526 tagNum++ 527 } 528 } 529 530 // Write out the final tag since the for loop above will miss anything 531 // after the final separator. Note, that we make sure that the final 532 // character in the name is not the separator because in that case there 533 // would be no additional tag to add. I.E if the input was: 534 // foo.bar.baz 535 // then the for loop would append foo and bar, but we would still need to 536 // append baz, however, if the input was: 537 // foo.bar.baz. 538 // then the foor loop would have appended foo, bar, and baz already. 539 if name[len(name)-1] != carbonSeparatorByte { 540 tags = append(tags, models.Tag{ 541 Name: graphite.TagName(tagNum), 542 Value: name[startIdx:], 543 }) 544 } 545 546 return models.Tags{Opts: opts, Tags: tags}, nil 547 } 548 549 // Compile all the carbon ingestion rules into matcher so that we can 550 // perform matching. Also, generate all the mapping rules and storage 551 // policies that we will need to pass to the DownsamplerAndWriter upfront 552 // so that we don't need to create them each time. 553 // 554 // Note that only one rule will be applied per metric and rules are applied 555 // such that the first one that matches takes precedence. As a result we need 556 // to make sure to maintain the order of the rules when we generate the compiled ones. 557 func (i *ingester) compileRulesWithLock(rules CarbonIngesterRules) ([]ruleAndMatcher, error) { 558 compiledRules := make([]ruleAndMatcher, 0, len(rules.Rules)) 559 for _, rule := range rules.Rules { 560 if rule.Pattern != "" && rule.Contains != "" { 561 return nil, fmt.Errorf( 562 "rule contains both pattern and contains: pattern=%s, contains=%s", 563 rule.Pattern, rule.Contains) 564 } 565 566 var ( 567 contains []byte 568 compiled *regexp.Regexp 569 ) 570 if rule.Contains != "" { 571 contains = []byte(rule.Contains) 572 } else { 573 var err error 574 compiled, err = regexp.Compile(rule.Pattern) 575 if err != nil { 576 return nil, err 577 } 578 } 579 580 storagePolicies := make([]policy.StoragePolicy, 0, len(rule.Policies)) 581 for _, currPolicy := range rule.Policies { 582 storagePolicy := policy.NewStoragePolicy( 583 currPolicy.Resolution, xtime.Second, currPolicy.Retention) 584 storagePolicies = append(storagePolicies, storagePolicy) 585 } 586 587 compiledRule := ruleAndMatcher{ 588 rule: rule, 589 contains: contains, 590 regexp: compiled, 591 } 592 593 if rule.Aggregation.EnabledOrDefault() { 594 compiledRule.mappingRules = []downsample.AutoMappingRule{ 595 { 596 Aggregations: []aggregation.Type{rule.Aggregation.TypeOrDefault()}, 597 Policies: storagePolicies, 598 }, 599 } 600 } else { 601 compiledRule.storagePolicies = storagePolicies 602 } 603 compiledRules = append(compiledRules, compiledRule) 604 } 605 606 return compiledRules, nil 607 } 608 609 func (i *ingester) getLineResources() *lineResources { 610 return i.lineResourcesPool.Get().(*lineResources) 611 } 612 613 func (i *ingester) putLineResources(l *lineResources) { 614 tooLargeForPool := cap(l.name) > maxResourcePoolNameSize || 615 len(l.datapoints) > 1 || // We always write one datapoint at a time. 616 cap(l.datapoints) > 1 || 617 cap(l.tags) > maxPooledTagsSize 618 619 if tooLargeForPool { 620 return 621 } 622 623 // Reset. 624 l.name = l.name[:0] 625 l.datapoints[0] = ts.Datapoint{} 626 for i := range l.tags { 627 // Free pointers. 628 l.tags[i] = models.Tag{} 629 } 630 l.tags = l.tags[:0] 631 632 i.lineResourcesPool.Put(l) 633 } 634 635 type lineResources struct { 636 name []byte 637 datapoints []ts.Datapoint 638 tags []models.Tag 639 } 640 641 type ruleAndMatcher struct { 642 rule config.CarbonIngesterRuleConfiguration 643 regexp *regexp.Regexp 644 contains []byte 645 mappingRules []downsample.AutoMappingRule 646 storagePolicies []policy.StoragePolicy 647 }