Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New module windows for metricbeat #3758

Merged
merged 18 commits into from
Mar 28, 2017
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions metricbeat/include/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ import (
_ "github.com/elastic/beats/metricbeat/module/system/network"
_ "github.com/elastic/beats/metricbeat/module/system/process"
_ "github.com/elastic/beats/metricbeat/module/system/socket"
_ "github.com/elastic/beats/metricbeat/module/windows"
_ "github.com/elastic/beats/metricbeat/module/windows/perfmon"
_ "github.com/elastic/beats/metricbeat/module/zookeeper"
_ "github.com/elastic/beats/metricbeat/module/zookeeper/mntr"
)
5 changes: 5 additions & 0 deletions metricbeat/module/windows/_meta/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#- module: windows
# metricsets: ["perfmon"]
# enabled: true
# period: 10s
# perfmon.counters:
3 changes: 3 additions & 0 deletions metricbeat/module/windows/_meta/docs.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
== windows Module

This is the windows Module.
10 changes: 10 additions & 0 deletions metricbeat/module/windows/_meta/fields.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
- key: windows
title: "Windows"
description: >
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[]beta
Module for Windows
fields:
- name: windows
type: group
description: >
fields:
4 changes: 4 additions & 0 deletions metricbeat/module/windows/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/*
Package windows is a Metricbeat module that contains MetricSets.
*/
package windows
19 changes: 19 additions & 0 deletions metricbeat/module/windows/perfmon/_meta/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"@timestamp":"2016-05-23T08:05:34.853Z",
"beat":{
"hostname":"beathost",
"name":"beathost"
},
"metricset":{
"host":"localhost",
"module":"mysql",
"name":"status",
"rtt":44269
},
"windows":{
"perfmon":{
"example": "perfmon"
}
},
"type":"metricsets"
}
26 changes: 26 additions & 0 deletions metricbeat/module/windows/perfmon/_meta/docs.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
=== windows perfmon MetricSet

This is the perfmon metricset of the module windows.

[float]
=== Features and configuration

Metricset to collect performance counters.
Example configuration:
```
- module: windows
metricsets: ["perfmon"]
enabled: true
period: 10s
perfmon.counters:
- group: "processor"
collectors:
- alias: "processor_perfomance"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for sharing the event output: #3758 (comment) Based on the output I was thinking if we should this key or field instead of alias?

Do we need the "group" part, or could we just let the user define processor. processor_perfomance and we would have the same outcome? Are there some additional functionalities of the groups except for the namespacing?

Other thing I'm thinking about is how far we can "align" the configs of jmx and perfmon as they try to tackle a similar problem. Here some examples: https://github.com/elastic/beats/blob/master/metricbeat/module/jolokia/jmx/_meta/docs.asciidoc Probably it is best to unify in a second step and first get your PR in ;-)

Copy link
Contributor Author

@martinscholz83 martinscholz83 Mar 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the "group" part, or could we just let the user define processor. processor_perfomance

I need the group param to get the name. See here v.Name. Or is there an easier way to extract value from config file?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand the code, the group name is only needed to create the Event but not for the API request to fetch the event. If you use common.MapStr.Put("my.key.is.nested") you will also get a nested object.

So my question is if it would also work, if the config would look as following:

- module: windows
  metricsets: ["perfmon"]
  enabled: true
  period: 10s
  perfmon.counters:
    - alias: "processor_perfomance"
      query: '\Processor Information(_Total)\% Processor Performance'
    - alias: "processor_time"
      query: "\\Processor Information(_Total)\\% Performance Limit"
    - alias: "disk.bytes_written"
      query: '\FileSystem Disk Activity(_Total)\FileSystem Bytes Written'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right. Group name is only for the event. To added to the alias field would be an option disk.bytes_written. But this assumes that the user have to added for all the counters he wants to group. disk.counter1, disk.counter2. My concerns are if you have a lot of counters in one group it's easier to make a mistake disk.counter1, dsk.counter2

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But you are right, it looks to complicated. I remove the field and built the event with the alias.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is definitively an issue with my suggestion. The benefit is that the user as the full flexibility on where to put events and groups are not strictly required. Also there is less indentation yaml mess that can happen.

If it is natural to have a group for the events and users kind of expect that in the result your approach definitively feels more natural.

@andrewkroh Any thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would let you decide which is better because of your experience.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm OK with the current config approach. But I haven't tried adding additional counters yet. Let's get some people using it and see what kind of feedback we get.

query: '\Processor Information(_Total)\% Processor Performance'
- alias: "processor_time"
query: '\Processor Information(_Total)\% Processor Time'
- group: "disk"
collectors:
- alias: "bytes_written"
query: '\FileSystem Disk Activity(_Total)\FileSystem Bytes Written'
```
26 changes: 26 additions & 0 deletions metricbeat/module/windows/perfmon/_meta/fields.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
- name: perfmon
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For dynamic metricsets, this will change so what we nomrally do is just define windows (module) part and leave the rest empty. Check the jmx metricset as an example.

type: group
fields:
- name: counters
type: group
description: >
Grouping of different counters
fields:
- name: group
type: string
description: >
Name of the group. For example `processor` or `disk`

- name: collectors
type: group
fields:
- name: alias
type: string
description: >
Short form for the query

- name: query
type: string
description: >
The query. For example `\\Processor Information(_Total)\\% Processor Performance`. Backslashes have to be escaped.

31 changes: 31 additions & 0 deletions metricbeat/module/windows/perfmon/defs_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// +build ignore

package perfmon

/*
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <pdh.h>
#include <pdhmsg.h>
#cgo LDFLAGS: -lpdh
*/
import "C"

const (
ERROR_SUCCESS = C.ERROR_SUCCESS
PDH_STATUS_VALID_DATA = C.PDH_CSTATUS_VALID_DATA
PDH_STATUS_NEW_DATA = C.PDH_CSTATUS_NEW_DATA
PDH_NO_DATA = C.PDH_NO_DATA
PDH_STATUS_NO_OBJECT = C.PDH_CSTATUS_NO_OBJECT
PDH_STATUS_NO_COUNTER = C.PDH_CSTATUS_NO_COUNTER
PDH_STATUS_INVALID_DATA = C.PDH_CSTATUS_INVALID_DATA
PDH_INVALID_HANDLE = C.PDH_INVALID_HANDLE
PDH_INVALID_DATA = C.PDH_INVALID_DATA
PDH_NO_MORE_DATA = C.PDH_NO_MORE_DATA
PdhFmtDouble = C.PDH_FMT_DOUBLE
PdhFmtLarge = C.PDH_FMT_LARGE
PdhFmtLong = C.PDH_FMT_LONG
)

type PdhCounterValue C.PDH_FMT_COUNTERVALUE
24 changes: 24 additions & 0 deletions metricbeat/module/windows/perfmon/defs_windows_386.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package perfmon

const (
ERROR_SUCCESS = 0x0
PDH_STATUS_VALID_DATA = 0x0
PDH_STATUS_NEW_DATA = 0x1
PDH_NO_DATA = 0x800007d5
PDH_STATUS_NO_OBJECT = 0xc0000bb8
PDH_STATUS_NO_COUNTER = 0xc0000bb9
PDH_STATUS_INVALID_DATA = 0xc0000bba
PDH_INVALID_HANDLE = 0xc0000bbc
PDH_INVALID_DATA = 0xc0000bc6
PDH_NO_MORE_DATA = 0xc0000bcc
PdhFmtDouble = 0x00000200
PdhFmtLarge = 0x00000400
PdhFmtLong = 0x00000100
)

type PdhCounterValue struct {
CStatus uint32
Pad_cgo_0 [4]byte
LongValue int32
Pad_cgo_1 [4]byte
}
24 changes: 24 additions & 0 deletions metricbeat/module/windows/perfmon/defs_windows_amd64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package perfmon

const (
ERROR_SUCCESS = 0x0
PDH_STATUS_VALID_DATA = 0x0
PDH_STATUS_NEW_DATA = 0x1
PDH_NO_DATA = 0x800007d5
PDH_STATUS_NO_OBJECT = 0xc0000bb8
PDH_STATUS_NO_COUNTER = 0xc0000bb9
PDH_STATUS_INVALID_DATA = 0xc0000bba
PDH_INVALID_HANDLE = 0xc0000bbc
PDH_INVALID_DATA = 0xc0000bc6
PDH_NO_MORE_DATA = 0xc0000bcc
PdhFmtDouble = 0x00000200
PdhFmtLarge = 0x00000400
PdhFmtLong = 0x00000100
)

type PdhCounterValue struct {
CStatus uint32
Pad_cgo_0 [4]byte
LongValue int32
Pad_cgo_1 [4]byte
}
120 changes: 120 additions & 0 deletions metricbeat/module/windows/perfmon/pdh_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package perfmon

import (
"strconv"
"unsafe"

"time"

"github.com/elastic/beats/libbeat/common"
)

type Handle struct {
status error
query uintptr
counterType int
counters []CounterGroup
}

type CounterGroup struct {
GroupName string
Counters []Counter
}

type Counter struct {
counterName string
counter uintptr
counterPath string
displayValue PdhCounterValue
}

var errorMapping = map[uint32]string{
PDH_INVALID_DATA: `PDH_INVALID_DATA`,
PDH_INVALID_HANDLE: `PDH_INVALID_HANDLE`,
PDH_NO_DATA: `PDH_NO_DATA`,
PDH_NO_MORE_DATA: `PDH_NO_MORE_DATA`,
PDH_STATUS_INVALID_DATA: `PDH_STATUS_INVALID_DATA`,
PDH_STATUS_NEW_DATA: `PDH_STATUS_NEW_DATA`,
PDH_STATUS_NO_COUNTER: `PDH_STATUS_NO_COUNTER`,
PDH_STATUS_NO_OBJECT: `PDH_STATUS_NO_OBJECT`,
}

func GetHandle(config []CounterConfig) (*Handle, uint32) {
q := &Handle{}
err := _PdhOpenQuery(0, 0, &q.query)
if err != ERROR_SUCCESS {
return nil, err
}

counterGroups := make([]CounterGroup, len(config))
q.counters = counterGroups

for i, v := range config {
counterGroups[i] = CounterGroup{GroupName: v.Name, Counters: make([]Counter, len(v.Group))}
for j, v1 := range v.Group {
counterGroups[i].Counters[j] = Counter{counterName: v1.Alias, counterPath: v1.Query}
err := _PdhAddCounter(q.query, counterGroups[i].Counters[j].counterPath, 0, &counterGroups[i].Counters[j].counter)
if err != ERROR_SUCCESS {
return nil, err
}
}
}

return q, 0
}

func (q *Handle) ReadData(firstFetch bool) (common.MapStr, uint32) {

err := _PdhCollectQueryData(q.query)

if firstFetch {
// Most counters require two sample values in order to compute a displayable value. So wait and then collect the second value
time.Sleep(2000)
err = _PdhCollectQueryData(q.query)
}

if err != ERROR_SUCCESS {
return nil, err
}

result := common.MapStr{}

for _, v := range q.counters {
groupVal := make(map[string]interface{})
for _, v1 := range v.Counters {
err := _PdhGetFormattedCounterValue(v1.counter, PdhFmtDouble, q.counterType, &v1.displayValue)
if err != ERROR_SUCCESS {
return nil, err
}
doubleValue := (*float64)(unsafe.Pointer(&v1.displayValue.LongValue))
groupVal[v1.counterName] = *doubleValue

}
result[v.GroupName] = groupVal
}
return result, 0
}

func CloseQuery(q uintptr) uint32 {
err := _PdhCloseQuery(q)
if err != ERROR_SUCCESS {
return err
}

return 0
}

func GetError(err uint32) string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are close to defining your own error type here. Consider doing something like this:

type PdhError uint32

Change GetError to func (e PdhError) Error() string. That makes PdhError satisfy the error interface and you and then return it from functions that user error.

Any place you have a uint32 error code you can cast it like PdhError(2).

if val, ok := errorMapping[err]; ok {
return val
}
return strconv.FormatUint(uint64(err), 10)
}

//go:generate go run $GOROOT/src/syscall/mksyscall_windows.go -output syscall_windows.go pdh.go
// Windows API calls
//sys _PdhOpenQuery(dataSource uintptr, userData uintptr, query *uintptr) (err uint32) = pdh.PdhOpenQuery
//sys _PdhAddCounter(query uintptr, counterPath string, userData uintptr, counter *uintptr) (err uint32) = pdh.PdhAddEnglishCounterW
//sys _PdhCollectQueryData(query uintptr) (err uint32) = pdh.PdhCollectQueryData
//sys _PdhGetFormattedCounterValue(counter uintptr, format uint32, counterType int, value *PdhCounterValue) (err uint32) = pdh.PdhGetFormattedCounterValue
//sys _PdhCloseQuery(query uintptr) (err uint32) = pdh.PdhCloseQuery
58 changes: 58 additions & 0 deletions metricbeat/module/windows/perfmon/pdh_windows_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//+build integration windows

package perfmon

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestExistingCounter(t *testing.T) {
config := make([]CounterConfig, 1)
config[0].Name = "process"
config[0].Group = make([]CounterConfigGroup, 1)
config[0].Group[0].Alias = "processor_performance"
config[0].Group[0].Query = "\\Processor Information(_Total)\\% Processor Performance"
handle, err := GetHandle(config)

assert.Empty(t, err)

err = CloseQuery(handle.query)

assert.Empty(t, err)
}

func TestNonExistingCounter(t *testing.T) {
config := make([]CounterConfig, 1)
config[0].Name = "process"
config[0].Group = make([]CounterConfigGroup, 1)
config[0].Group[0].Alias = "processor_performance"
config[0].Group[0].Query = "\\Processor Information(_Total)\\not existing counter"
handle, err := GetHandle(config)

assert.Equal(t, 3221228473, int(err))

if handle != nil {
err = CloseQuery(handle.query)

assert.Empty(t, err)
}
}

func TestNonExistingObject(t *testing.T) {
config := make([]CounterConfig, 1)
config[0].Name = "process"
config[0].Group = make([]CounterConfigGroup, 1)
config[0].Group[0].Alias = "processor_performance"
config[0].Group[0].Query = "\\non existing object\\% Processor Performance"
handle, err := GetHandle(config)

assert.Equal(t, 3221228472, int(err))

if handle != nil {
err = CloseQuery(handle.query)

assert.Empty(t, err)
}
}
Loading