scraper.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. // Copyright The OpenTelemetry Authors
  2. // SPDX-License-Identifier: Apache-2.0
  3. package snmpreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/snmpreceiver"
  4. import (
  5. "context"
  6. "errors"
  7. "fmt"
  8. "strconv"
  9. "strings"
  10. "time"
  11. "go.opentelemetry.io/collector/component"
  12. "go.opentelemetry.io/collector/pdata/pcommon"
  13. "go.opentelemetry.io/collector/pdata/pmetric"
  14. "go.opentelemetry.io/collector/receiver"
  15. "go.opentelemetry.io/collector/receiver/scrapererror"
  16. "go.uber.org/zap"
  17. )
  18. var (
  19. // Error messages
  20. errMsgBadValueType = `returned metric SNMP data type for OID '%s' is not supported`
  21. errMsgIndexedAttributesBadValueType = `returned attribute SNMP data type for OID '%s' from column OID '%s' is not supported`
  22. errMsgScalarAttributesBadValueType = `returned attribute SNMP data type for OID '%s' is not supported`
  23. errMsgOIDAttributeEmptyValue = `not creating indexed metric '%s' datapoint: %w`
  24. errMsgAttributeEmptyValue = `metric OID attribute value is blank`
  25. errMsgResourceAttributeEmptyValue = `related resource attribute value is blank`
  26. errMsgOIDResourceAttributeEmptyValue = `not creating indexed metric '%s' or resource: %w`
  27. errMsgScalarOIDProcessing = `problem processing scalar metric data for OID '%s': %w`
  28. errMsgIndexedMetricOIDProcessing = `problem processing indexed metric data for OID '%s' from column OID '%s': %w`
  29. errMsgScalarAttributeOIDProcessing = `problem processing scalar attribute data from scalar OID '%s': %w`
  30. errMsgIndexedAttributeOIDProcessing = `problem processing indexed attribute data for OID '%s' from column OID '%s': %w`
  31. )
  32. // snmpScraper handles scraping of SNMP metrics
  33. type snmpScraper struct {
  34. client client
  35. logger *zap.Logger
  36. cfg *Config
  37. settings receiver.CreateSettings
  38. startTime pcommon.Timestamp
  39. }
  40. type indexedAttributeValues map[string]string
  41. // newScraper creates an initialized snmpScraper
  42. func newScraper(logger *zap.Logger, cfg *Config, settings receiver.CreateSettings) *snmpScraper {
  43. return &snmpScraper{
  44. logger: logger,
  45. cfg: cfg,
  46. settings: settings,
  47. }
  48. }
  49. // start gets the client ready
  50. func (s *snmpScraper) start(_ context.Context, _ component.Host) (err error) {
  51. s.client, err = newClient(s.cfg, s.logger)
  52. s.startTime = pcommon.NewTimestampFromTime(time.Now())
  53. return err
  54. }
  55. // scrape collects and creates OTEL metrics from a SNMP environment
  56. func (s *snmpScraper) scrape(_ context.Context) (pmetric.Metrics, error) {
  57. if err := s.client.Connect(); err != nil {
  58. return pmetric.NewMetrics(), fmt.Errorf("problem connecting to SNMP host: %w", err)
  59. }
  60. defer s.client.Close()
  61. // Create the metrics helper which will help manage a lot of the otel metric and resource functionality
  62. metricHelper := newOTELMetricHelper(s.settings, s.startTime)
  63. configHelper := newConfigHelper(s.cfg)
  64. var scraperErrors scrapererror.ScrapeErrors
  65. // Try to scrape scalar OID based metrics
  66. s.scrapeScalarMetrics(metricHelper, configHelper, &scraperErrors)
  67. // Try to scrape column OID based metrics
  68. s.scrapeIndexedMetrics(metricHelper, configHelper, &scraperErrors)
  69. return metricHelper.metrics, scraperErrors.Combine()
  70. }
  71. // scrapeScalarMetrics retrieves all SNMP data from scalar OIDs and turns the returned scalar data
  72. // into metrics with optional enum attributes
  73. func (s *snmpScraper) scrapeScalarMetrics(
  74. metricHelper *otelMetricHelper,
  75. configHelper *configHelper,
  76. scraperErrors *scrapererror.ScrapeErrors,
  77. ) {
  78. metricScalarOIDs := configHelper.getMetricScalarOIDs()
  79. // If no scalar metric configs, nothing else to do
  80. if len(metricScalarOIDs) == 0 {
  81. return
  82. }
  83. // Retrieve all SNMP data from scalar metric OIDs
  84. scalarData := s.client.GetScalarData(metricScalarOIDs, scraperErrors)
  85. // If no scalar data, nothing else to do
  86. if len(scalarData) == 0 {
  87. return
  88. }
  89. // Retrieve scalar OID SNMP data for resource attributes
  90. scalarResourceAttributes := s.scrapeScalarResourceAttributes(configHelper.getResourceAttributeScalarOIDs(), scraperErrors)
  91. // For each piece of SNMP data, attempt to create the necessary OTEL structures (resources/metrics/datapoints)
  92. for _, data := range scalarData {
  93. if err := s.scalarDataToMetric(data, metricHelper, configHelper, scalarResourceAttributes); err != nil {
  94. scraperErrors.AddPartial(1, fmt.Errorf(errMsgScalarOIDProcessing, data.oid, err))
  95. }
  96. }
  97. }
  98. // scrapeIndexedMetrics retrieves all SNMP data from column OIDs and turns the returned indexed data
  99. // into metrics with optional attribute and/or resource attributes
  100. func (s *snmpScraper) scrapeIndexedMetrics(
  101. metricHelper *otelMetricHelper,
  102. configHelper *configHelper,
  103. scraperErrors *scrapererror.ScrapeErrors,
  104. ) {
  105. metricColumnOIDs := configHelper.getMetricColumnOIDs()
  106. // If no column metric configs, nothing else to do
  107. if len(metricColumnOIDs) == 0 {
  108. return
  109. }
  110. // Retrieve column OID SNMP indexed data for attributes
  111. columnOIDIndexedAttributeValues := s.scrapeIndexedAttributes(configHelper.getAttributeColumnOIDs(), scraperErrors)
  112. // Retrieve column OID SNMP indexed data for resource attributes
  113. columnOIDIndexedResourceAttributeValues := s.scrapeIndexedAttributes(configHelper.getResourceAttributeColumnOIDs(), scraperErrors)
  114. // Retrieve scalar OID SNMP data for resource attributes
  115. columnOIDScalarOIDResourceAttributeValues := s.scrapeScalarResourceAttributes(configHelper.getResourceAttributeScalarOIDs(), scraperErrors)
  116. // Retrieve all SNMP indexed data from column metric OIDs
  117. indexedData := s.client.GetIndexedData(metricColumnOIDs, scraperErrors)
  118. // For each piece of SNMP data, attempt to create the necessary OTEL structures (resources/metrics/datapoints)
  119. for _, data := range indexedData {
  120. if err := s.indexedDataToMetric(data, metricHelper, configHelper, columnOIDIndexedAttributeValues, columnOIDIndexedResourceAttributeValues, columnOIDScalarOIDResourceAttributeValues); err != nil {
  121. scraperErrors.AddPartial(1, fmt.Errorf(errMsgIndexedMetricOIDProcessing, data.oid, data.columnOID, err))
  122. }
  123. }
  124. }
  125. // scalarDataToMetric will take one piece of SNMP scalar data and turn it into a datapoint for
  126. // either a new or existing metric with attributes based on the related configs
  127. func (s *snmpScraper) scalarDataToMetric(
  128. data SNMPData,
  129. metricHelper *otelMetricHelper,
  130. configHelper *configHelper,
  131. scalarResourceAttributes map[string]string,
  132. ) error {
  133. // Get the related metric name for this SNMP indexed data
  134. metricName := configHelper.getMetricName(data.oid)
  135. // Keys will be determined from the related attribute config and enum values will come straight from
  136. // the metric config's attribute values.
  137. dataPointAttributes := getScalarDataPointAttributes(configHelper, data.oid)
  138. // Get resource attributes
  139. resourceAttributes, err := getResourceAttributes(configHelper, data.oid, "0", map[string]indexedAttributeValues{}, scalarResourceAttributes)
  140. if err != nil {
  141. return fmt.Errorf(errMsgOIDResourceAttributeEmptyValue, metricName, err)
  142. }
  143. // Create a resource key using all of the relevant resource attribute names
  144. resourceAttributeNames := configHelper.getResourceAttributeNames(data.oid)
  145. var resourceKey string
  146. if len(resourceAttributeNames) > 0 {
  147. resourceKey = getResourceKey(resourceAttributeNames, "")
  148. } else {
  149. // Create general resource if we don't have any resource attributes
  150. resourceKey = generalResourceKey
  151. }
  152. // Create a new resource if needed
  153. resource := metricHelper.getResource(resourceKey)
  154. if resource == nil {
  155. metricHelper.createResource(resourceKey, resourceAttributes)
  156. }
  157. return addMetricDataPointToResource(data, metricHelper, configHelper, metricName, resourceKey, dataPointAttributes)
  158. }
  159. // indexedDataToMetric will take one piece of column OID SNMP indexed metric data and turn it
  160. // into a datapoint for either a new or existing metric with attributes that belongs to either
  161. // a new or existing resource
  162. func (s *snmpScraper) indexedDataToMetric(
  163. data SNMPData,
  164. metricHelper *otelMetricHelper,
  165. configHelper *configHelper,
  166. columnOIDIndexedAttributeValues map[string]indexedAttributeValues,
  167. columnOIDIndexedResourceAttributeValues map[string]indexedAttributeValues,
  168. columnOIDScalarResourceAttributeValues map[string]string,
  169. ) error {
  170. // Get the related metric name for this SNMP indexed data
  171. metricName := configHelper.getMetricName(data.columnOID)
  172. indexString := strings.TrimPrefix(data.oid, data.columnOID)
  173. // Get data point attributes
  174. dataPointAttributes, err := getIndexedDataPointAttributes(configHelper, data.columnOID, indexString, columnOIDIndexedAttributeValues)
  175. if err != nil {
  176. return fmt.Errorf(errMsgOIDAttributeEmptyValue, metricName, err)
  177. }
  178. // Get resource attributes
  179. resourceAttributes, err := getResourceAttributes(configHelper, data.columnOID, indexString, columnOIDIndexedResourceAttributeValues, columnOIDScalarResourceAttributeValues)
  180. if err != nil {
  181. return fmt.Errorf(errMsgOIDResourceAttributeEmptyValue, metricName, err)
  182. }
  183. // Create a resource key using all of the relevant resource attribute names along
  184. // with the row index of the SNMP data
  185. resourceAttributeNames := configHelper.getResourceAttributeNames(data.columnOID)
  186. // Check how many of the resource attributes on this metric are scalar
  187. var numScalarResourceAttributes int
  188. for name := range resourceAttributes {
  189. if s.cfg.ResourceAttributes[name].ScalarOID != "" {
  190. numScalarResourceAttributes++
  191. }
  192. }
  193. var resourceKey string
  194. // If the only resource attributes on this metric are scalar, we don't need multiple resources
  195. if len(resourceAttributes) == numScalarResourceAttributes {
  196. resourceKey = getResourceKey(resourceAttributeNames, "")
  197. } else {
  198. resourceKey = getResourceKey(resourceAttributeNames, indexString)
  199. }
  200. // Create a new resource if needed
  201. resource := metricHelper.getResource(resourceKey)
  202. if resource == nil {
  203. metricHelper.createResource(resourceKey, resourceAttributes)
  204. }
  205. return addMetricDataPointToResource(data, metricHelper, configHelper, metricName, resourceKey, dataPointAttributes)
  206. }
  207. func addMetricDataPointToResource(
  208. data SNMPData,
  209. metricHelper *otelMetricHelper,
  210. configHelper *configHelper,
  211. metricName string,
  212. resourceKey string,
  213. dataPointAttributes map[string]string,
  214. ) error {
  215. // Return an error if this SNMP indexed data is not of a useable type
  216. if data.valueType == notSupportedVal || data.valueType == stringVal {
  217. return fmt.Errorf(errMsgBadValueType, data.oid)
  218. }
  219. // Get the related metric config
  220. metricCfg := configHelper.getMetricConfig(metricName)
  221. // Create a new metric if needed
  222. if metric := metricHelper.getMetric(resourceKey, metricName); metric == nil {
  223. if _, err := metricHelper.createMetric(resourceKey, metricName, metricCfg); err != nil {
  224. return err
  225. }
  226. }
  227. // Add data point to metric
  228. if _, err := metricHelper.addMetricDataPoint(resourceKey, metricName, metricCfg, data, dataPointAttributes); err != nil {
  229. return err
  230. }
  231. return nil
  232. }
  233. // getScalarDataPointAttributes returns the key value pairs of attributes for a given metric config scalar OID
  234. func getScalarDataPointAttributes(configHelper *configHelper, oid string) map[string]string {
  235. dataPointAttributes := map[string]string{}
  236. for _, attribute := range configHelper.metricAttributesByOID[oid] {
  237. attributeKey := attribute.Name
  238. if value := configHelper.getAttributeConfigValue(attributeKey); value != "" {
  239. attributeKey = value
  240. }
  241. dataPointAttributes[attributeKey] = attribute.Value
  242. }
  243. return dataPointAttributes
  244. }
  245. // getIndexedDataPointAttributes gets attributes for this metric's datapoint based on the previously
  246. // gathered attributes.
  247. // Keys will be determined from the related attribute config and values will come a few
  248. // different places.
  249. // Enum attribute value - comes from the metric config's attribute data
  250. // Indexed prefix attribute value - comes from the current SNMP data's index and the attribute
  251. // config's prefix value
  252. // Indexed OID attribute value - comes from the previously collected indexed attribute data
  253. // using the current index and attribute config to access the correct value
  254. func getIndexedDataPointAttributes(
  255. configHelper *configHelper,
  256. columnOID string,
  257. indexString string,
  258. columnOIDIndexedAttributeValues map[string]indexedAttributeValues,
  259. ) (map[string]string, error) {
  260. datapointAttributes := map[string]string{}
  261. for _, attribute := range configHelper.getMetricConfigAttributes(columnOID) {
  262. attributeName := attribute.Name
  263. attributeKey := attributeName
  264. // Use alternate attribute key if available
  265. if value := configHelper.getAttributeConfigValue(attributeKey); value != "" {
  266. attributeKey = value
  267. }
  268. var attributeValue string
  269. prefix := configHelper.getAttributeConfigIndexedValuePrefix(attributeName)
  270. oid := configHelper.getAttributeConfigOID(attributeName)
  271. switch {
  272. case prefix != "":
  273. attributeValue = prefix + indexString
  274. case oid != "":
  275. attributeValue = columnOIDIndexedAttributeValues[oid][indexString]
  276. default:
  277. attributeValue = attribute.Value
  278. }
  279. // If no good attribute value could be found
  280. if attributeValue == "" {
  281. return nil, errors.New(errMsgAttributeEmptyValue)
  282. }
  283. datapointAttributes[attributeKey] = attributeValue
  284. }
  285. return datapointAttributes, nil
  286. }
  287. // getResourceAttributes creates a map of key/values for all related resource attributes. Keys
  288. // will come directly from the metric config's resource attribute values. Values will come
  289. // from the related attribute config's prefix value plus the index OR the previously collected
  290. // resource attribute indexed data.
  291. func getResourceAttributes(
  292. configHelper *configHelper,
  293. columnOID string,
  294. indexString string,
  295. columnOIDIndexedResourceAttributeValues map[string]indexedAttributeValues,
  296. columnOIDScalarResourceAttributeValues map[string]string,
  297. ) (map[string]string, error) {
  298. resourceAttributes := map[string]string{}
  299. for _, attributeName := range configHelper.getResourceAttributeNames(columnOID) {
  300. prefix := configHelper.getResourceAttributeConfigIndexedValuePrefix(attributeName)
  301. oid := configHelper.getResourceAttributeConfigOID(attributeName)
  302. scalarOid := configHelper.getResourceAttributeConfigScalarOID(attributeName)
  303. switch {
  304. case prefix != "":
  305. resourceAttributes[attributeName] = prefix + indexString
  306. case oid != "":
  307. attributeValue := columnOIDIndexedResourceAttributeValues[oid][indexString]
  308. if attributeValue == "" {
  309. return nil, errors.New(errMsgResourceAttributeEmptyValue)
  310. }
  311. resourceAttributes[attributeName] = attributeValue
  312. case scalarOid != "":
  313. resourceAttributes[attributeName] = columnOIDScalarResourceAttributeValues[scalarOid]
  314. default:
  315. return nil, errors.New(errMsgResourceAttributeEmptyValue)
  316. }
  317. }
  318. return resourceAttributes, nil
  319. }
  320. // scrapeScalarResourceAttributes retrieves all SNMP data from resource attribute
  321. // config scalar OIDs and stores the returned data for later use by metrics
  322. func (s *snmpScraper) scrapeScalarResourceAttributes(
  323. scalarOIDs []string,
  324. scraperErrors *scrapererror.ScrapeErrors,
  325. ) map[string]string {
  326. scalarOIDAttributeValues := make(map[string]string, len(scalarOIDs))
  327. // If no scalar OID resource attribute configs, nothing else to do
  328. if len(scalarOIDs) == 0 {
  329. return scalarOIDAttributeValues
  330. }
  331. // Retrieve all SNMP data from scalar resource attribute OIDs
  332. scalarData := s.client.GetScalarData(scalarOIDs, scraperErrors)
  333. // For each piece of SNMP data, store the necessary info to help create resources later if needed
  334. for _, data := range scalarData {
  335. if err := scalarDataToResourceAttribute(data, scalarOIDAttributeValues); err != nil {
  336. scraperErrors.AddPartial(1, fmt.Errorf(errMsgScalarAttributeOIDProcessing, data.oid, err))
  337. }
  338. }
  339. return scalarOIDAttributeValues
  340. }
  341. // scalarDataToResourceAttribute provides a function which will take one piece of scalar OID SNMP data
  342. // (for a resource attribute) and store it in a map for later use
  343. func scalarDataToResourceAttribute(
  344. data SNMPData,
  345. scalarOIDAttributeValues map[string]string,
  346. ) error {
  347. // Get the string value of the SNMP data for the {resource} attribute value
  348. var stringValue string
  349. // Not explicitly checking these casts as this should be made safe in the client
  350. switch data.valueType {
  351. case notSupportedVal:
  352. return fmt.Errorf(errMsgScalarAttributesBadValueType, data.oid)
  353. case stringVal:
  354. stringValue = data.value.(string)
  355. case integerVal:
  356. stringValue = strconv.FormatInt(data.value.(int64), 10)
  357. case floatVal:
  358. stringValue = strconv.FormatFloat(data.value.(float64), 'f', 2, 64)
  359. }
  360. // Store the {resource} attribute value in a map using the scalar OID as a key.
  361. // This way we can match metrics to this data through the {resource} attribute config.
  362. scalarOIDAttributeValues[data.oid] = stringValue
  363. return nil
  364. }
  365. // scrapeIndexedAttributes retrieves all SNMP data from attribute (or resource attribute)
  366. // config column OIDs and stores the returned indexed data for later use by metrics
  367. func (s *snmpScraper) scrapeIndexedAttributes(
  368. columnOIDs []string,
  369. scraperErrors *scrapererror.ScrapeErrors,
  370. ) map[string]indexedAttributeValues {
  371. columnOIDIndexedAttributeValues := map[string]indexedAttributeValues{}
  372. // If no OID resource attribute configs, nothing else to do
  373. if len(columnOIDs) == 0 {
  374. return columnOIDIndexedAttributeValues
  375. }
  376. // Retrieve all SNMP indexed data from column resource attribute OIDs
  377. indexedData := s.client.GetIndexedData(columnOIDs, scraperErrors)
  378. // For each piece of SNMP data, store the necessary info to help create resources later if needed
  379. for _, data := range indexedData {
  380. if err := indexedDataToAttribute(data, columnOIDIndexedAttributeValues); err != nil {
  381. scraperErrors.AddPartial(1, fmt.Errorf(errMsgIndexedAttributeOIDProcessing, data.oid, data.columnOID, err))
  382. }
  383. }
  384. return columnOIDIndexedAttributeValues
  385. }
  386. // indexedDataToAttribute provides a function which will take one piece of column OID SNMP indexed data
  387. // (for either an attribute or resource attribute) and stores it in a map for later use (keyed by both
  388. // {resource} attribute config column OID and OID index)
  389. func indexedDataToAttribute(
  390. data SNMPData,
  391. columnOIDIndexedAttributeValues map[string]indexedAttributeValues,
  392. ) error {
  393. // Get the string value of the SNMP data for the {resource} attribute value
  394. var stringValue string
  395. // Not explicitly checking these casts as this should be made safe in the client
  396. switch data.valueType {
  397. case notSupportedVal:
  398. return fmt.Errorf(errMsgIndexedAttributesBadValueType, data.oid, data.columnOID)
  399. case stringVal:
  400. stringValue = data.value.(string)
  401. case integerVal:
  402. stringValue = strconv.FormatInt(data.value.(int64), 10)
  403. case floatVal:
  404. stringValue = strconv.FormatFloat(data.value.(float64), 'f', 2, 64)
  405. }
  406. // Store the {resource} attribute value in a map using the column OID and OID index associated
  407. // as keys. This way we can match indexed metrics to this data through the {resource} attribute
  408. // config and the indices of the individual metric values
  409. indexString := strings.TrimPrefix(data.oid, data.columnOID)
  410. if columnOIDIndexedAttributeValues[data.columnOID] == nil {
  411. columnOIDIndexedAttributeValues[data.columnOID] = indexedAttributeValues{}
  412. }
  413. columnOIDIndexedAttributeValues[data.columnOID][indexString] = stringValue
  414. return nil
  415. }