mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-02-08 14:06:38 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44c39405c7 | ||
|
|
96faedf481 | ||
|
|
3cfc11c6d2 | ||
|
|
80f1cf0546 | ||
|
|
691b672a1e | ||
|
|
2e0842573d | ||
|
|
7537c9896e | ||
|
|
88271ddf14 | ||
|
|
b0f1e1843b | ||
|
|
00f57c183d | ||
|
|
dc76c4227d | ||
|
|
f4195aa435 | ||
|
|
1ebee26c30 | ||
|
|
1db60e22b9 | ||
|
|
e8cfeef26c | ||
|
|
0c4c15c4ce | ||
|
|
175c54acf1 | ||
|
|
95b04ec0a1 | ||
|
|
2c84c5ad8a |
13
README.md
13
README.md
@@ -9,6 +9,7 @@ Prometheus exporter for Windows machines, using the WMI (Windows Management Inst
|
||||
|
||||
Name | Description | Enabled by default
|
||||
---------|-------------|--------------------
|
||||
ad | [Win32_PerfRawData_DirectoryServices_DirectoryServices](https://msdn.microsoft.com/en-us/library/ms803980.aspx) Active Directory |
|
||||
cpu | [Win32_PerfRawData_PerfOS_Processor](https://msdn.microsoft.com/en-us/library/aa394317(v=vs.90).aspx) metrics (cpu usage) | ✓
|
||||
cs | [Win32_ComputerSystem](https://msdn.microsoft.com/en-us/library/aa394102) metrics (system properties, num cpus/total memory) | ✓
|
||||
dns | [Win32_PerfRawData_DNS_DNS](https://technet.microsoft.com/en-us/library/cc977686.aspx) metrics (DNS Server) |
|
||||
@@ -16,6 +17,7 @@ iis | [Win32_PerfRawData_W3SVC_WebService](https://msdn.microsoft.com/en-us/libr
|
||||
logical_disk | [Win32_PerfRawData_PerfDisk_LogicalDisk](https://msdn.microsoft.com/en-us/windows/hardware/aa394307(v=vs.71)) metrics (disk I/O) | ✓
|
||||
net | [Win32_PerfRawData_Tcpip_NetworkInterface](https://technet.microsoft.com/en-us/security/aa394340(v=vs.80)) metrics (network interface I/O) | ✓
|
||||
os | [Win32_OperatingSystem](https://msdn.microsoft.com/en-us/library/aa394239) metrics (memory, processes, users) | ✓
|
||||
process | [Win32_PerfRawData_PerfProc_Process](https://msdn.microsoft.com/en-us/library/aa394323(v=vs.85).aspx) metrics (per-process stats) |
|
||||
service | [Win32_Service](https://msdn.microsoft.com/en-us/library/aa394418(v=vs.85).aspx) metrics (service states) | ✓
|
||||
system | Win32_PerfRawData_PerfOS_System metrics (system calls) | ✓
|
||||
vmware | Performance counters installed by the Vmware Guest agent |
|
||||
@@ -50,13 +52,22 @@ See [open issues](https://github.com/martinlindhe/wmi_exporter/issues)
|
||||
## Usage
|
||||
|
||||
go get -u github.com/kardianos/govendor
|
||||
go get -u github.com/prometheus/promu
|
||||
go get -u github.com/martinlindhe/wmi_exporter
|
||||
cd $env:GOPATH/src/github.com/martinlindhe/wmi_exporter
|
||||
govendor build +local
|
||||
promu build -v .
|
||||
.\wmi_exporter.exe
|
||||
|
||||
The prometheus metrics will be exposed on [localhost:9182](http://localhost:9182)
|
||||
|
||||
## Examples
|
||||
|
||||
Please note: The quotes in the parameter names are required because of how Powershell parses command line arguments.
|
||||
|
||||
### Enable only service collector and specify a custom query
|
||||
|
||||
.\wmi_exporter.exe "-collectors.enabled" "service" "-collector.service.services-where" "Name='wmi_exporter'"
|
||||
|
||||
|
||||
## License
|
||||
|
||||
|
||||
1308
collector/ad.go
Normal file
1308
collector/ad.go
Normal file
File diff suppressed because it is too large
Load Diff
1175
collector/iis.go
1175
collector/iis.go
File diff suppressed because it is too large
Load Diff
383
collector/process.go
Normal file
383
collector/process.go
Normal file
@@ -0,0 +1,383 @@
|
||||
// returns data points from Win32_PerfRawData_PerfProc_Process
|
||||
// https://msdn.microsoft.com/en-us/library/aa394323(v=vs.85).aspx - Win32_PerfRawData_PerfProc_Process class
|
||||
package collector
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/StackExchange/wmi"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Factories["process"] = NewProcessCollector
|
||||
}
|
||||
|
||||
var (
|
||||
processWhereClause = flag.String("collector.process.processes-where", "", "WQL 'where' clause to use in WMI metrics query. Limits the response to the processes you specify and reduces the size of the response.")
|
||||
)
|
||||
|
||||
// A ProcessCollector is a Prometheus collector for WMI Win32_PerfRawData_PerfProc_Process metrics
|
||||
type ProcessCollector struct {
|
||||
StartTime *prometheus.Desc
|
||||
CPUTimeTotal *prometheus.Desc
|
||||
HandleCount *prometheus.Desc
|
||||
IOBytesTotal *prometheus.Desc
|
||||
IOOperationsTotal *prometheus.Desc
|
||||
PageFaultsTotal *prometheus.Desc
|
||||
PageFileBytes *prometheus.Desc
|
||||
PoolBytes *prometheus.Desc
|
||||
PriorityBase *prometheus.Desc
|
||||
PrivateBytes *prometheus.Desc
|
||||
ThreadCount *prometheus.Desc
|
||||
VirtualBytes *prometheus.Desc
|
||||
WorkingSet *prometheus.Desc
|
||||
|
||||
queryWhereClause string
|
||||
}
|
||||
|
||||
// NewProcessCollector ...
|
||||
func NewProcessCollector() (Collector, error) {
|
||||
const subsystem = "process"
|
||||
|
||||
var wc bytes.Buffer
|
||||
if *processWhereClause != "" {
|
||||
wc.WriteString("WHERE ")
|
||||
wc.WriteString(*processWhereClause)
|
||||
} else {
|
||||
log.Println("warning: No where-clause specified for process collector. This will generate a very large number of metrics!")
|
||||
}
|
||||
|
||||
return &ProcessCollector{
|
||||
StartTime: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, subsystem, "start_time"),
|
||||
"Time of process start.",
|
||||
[]string{"process", "process_id", "creating_process_id"},
|
||||
nil,
|
||||
),
|
||||
CPUTimeTotal: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, subsystem, "cpu_time_total"),
|
||||
"Returns elapsed time that all of the threads of this process used the processor to execute instructions by mode (privileged, user). An instruction is the basic unit of execution in a computer, a thread is the object that executes instructions, and a process is the object created when a program is run. Code executed to handle some hardware interrupts and trap conditions is included in this count.",
|
||||
[]string{"process", "process_id", "creating_process_id", "mode"},
|
||||
nil,
|
||||
),
|
||||
HandleCount: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, subsystem, "handle_count"),
|
||||
"Total number of handles the process has open. This number is the sum of the handles currently open by each thread in the process.",
|
||||
[]string{"process", "process_id", "creating_process_id"},
|
||||
nil,
|
||||
),
|
||||
IOBytesTotal: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, subsystem, "io_bytes_total"),
|
||||
"Bytes issued to I/O operations in different modes (read, write, other). This property counts all I/O activity generated by the process to include file, network, and device I/Os. Read and write mode includes data operations; other mode includes those that do not involve data, such as control operations. ",
|
||||
[]string{"process", "process_id", "creating_process_id", "mode"},
|
||||
nil,
|
||||
),
|
||||
IOOperationsTotal: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, subsystem, "io_operations_total"),
|
||||
"I/O operations issued in different modes (read, write, other). This property counts all I/O activity generated by the process to include file, network, and device I/Os. Read and write mode includes data operations; other mode includes those that do not involve data, such as control operations. ",
|
||||
[]string{"process", "process_id", "creating_process_id", "mode"},
|
||||
nil,
|
||||
),
|
||||
PageFaultsTotal: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, subsystem, "page_faults_total"),
|
||||
"Page faults by the threads executing in this process. A page fault occurs when a thread refers to a virtual memory page that is not in its working set in main memory. This can cause the page not to be fetched from disk if it is on the standby list and hence already in main memory, or if it is in use by another process with which the page is shared.",
|
||||
[]string{"process", "process_id", "creating_process_id"},
|
||||
nil,
|
||||
),
|
||||
PageFileBytes: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, subsystem, "page_file_bytes"),
|
||||
"Current number of bytes this process has used in the paging file(s). Paging files are used to store pages of memory used by the process that are not contained in other files. Paging files are shared by all processes, and lack of space in paging files can prevent other processes from allocating memory.",
|
||||
[]string{"process", "process_id", "creating_process_id"},
|
||||
nil,
|
||||
),
|
||||
PoolBytes: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, subsystem, "pool_bytes"),
|
||||
"Pool Bytes is the last observed number of bytes in the paged or nonpaged pool. The nonpaged pool is an area of system memory (physical memory used by the operating system) for objects that cannot be written to disk, but must remain in physical memory as long as they are allocated. The paged pool is an area of system memory (physical memory used by the operating system) for objects that can be written to disk when they are not being used. Nonpaged pool bytes is calculated differently than paged pool bytes, so it might not equal the total of paged pool bytes.",
|
||||
[]string{"process", "process_id", "creating_process_id", "pool"},
|
||||
nil,
|
||||
),
|
||||
PriorityBase: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, subsystem, "priority_base"),
|
||||
"Current base priority of this process. Threads within a process can raise and lower their own base priority relative to the process base priority of the process.",
|
||||
[]string{"process", "process_id", "creating_process_id"},
|
||||
nil,
|
||||
),
|
||||
PrivateBytes: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, subsystem, "private_bytes"),
|
||||
"Current number of bytes this process has allocated that cannot be shared with other processes.",
|
||||
[]string{"process", "process_id", "creating_process_id"},
|
||||
nil,
|
||||
),
|
||||
ThreadCount: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, subsystem, "thread_count"),
|
||||
"Number of threads currently active in this process. An instruction is the basic unit of execution in a processor, and a thread is the object that executes instructions. Every running process has at least one thread.",
|
||||
[]string{"process", "process_id", "creating_process_id"},
|
||||
nil,
|
||||
),
|
||||
VirtualBytes: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, subsystem, "virtual_bytes"),
|
||||
"Current size, in bytes, of the virtual address space that the process is using. Use of virtual address space does not necessarily imply corresponding use of either disk or main memory pages. Virtual space is finite and, by using too much, the process can limit its ability to load libraries.",
|
||||
[]string{"process", "process_id", "creating_process_id"},
|
||||
nil,
|
||||
),
|
||||
WorkingSet: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, subsystem, "working_set"),
|
||||
"Maximum number of bytes in the working set of this process at any point in time. The working set is the set of memory pages touched recently by the threads in the process. If free memory in the computer is above a threshold, pages are left in the working set of a process even if they are not in use. When free memory falls below a threshold, pages are trimmed from working sets. If they are needed, they are then soft-faulted back into the working set before they leave main memory.",
|
||||
[]string{"process", "process_id", "creating_process_id"},
|
||||
nil,
|
||||
),
|
||||
queryWhereClause: wc.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Collect sends the metric values for each metric
|
||||
// to the provided prometheus Metric channel.
|
||||
func (c *ProcessCollector) Collect(ch chan<- prometheus.Metric) error {
|
||||
if desc, err := c.collect(ch); err != nil {
|
||||
log.Println("[ERROR] failed collecting process metrics:", desc, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Win32_PerfRawData_PerfProc_Process struct {
|
||||
Name string
|
||||
CreatingProcessID uint32
|
||||
ElapsedTime uint64
|
||||
Frequency_Object uint64
|
||||
HandleCount uint32
|
||||
IDProcess uint32
|
||||
IODataBytesPersec uint64
|
||||
IODataOperationsPersec uint64
|
||||
IOOtherBytesPersec uint64
|
||||
IOOtherOperationsPersec uint64
|
||||
IOReadBytesPersec uint64
|
||||
IOReadOperationsPersec uint64
|
||||
IOWriteBytesPersec uint64
|
||||
IOWriteOperationsPersec uint64
|
||||
PageFaultsPersec uint32
|
||||
PageFileBytes uint64
|
||||
PageFileBytesPeak uint64
|
||||
PercentPrivilegedTime uint64
|
||||
PercentProcessorTime uint64
|
||||
PercentUserTime uint64
|
||||
PoolNonpagedBytes uint32
|
||||
PoolPagedBytes uint32
|
||||
PriorityBase uint32
|
||||
PrivateBytes uint64
|
||||
ThreadCount uint32
|
||||
Timestamp_Object uint64
|
||||
VirtualBytes uint64
|
||||
VirtualBytesPeak uint64
|
||||
WorkingSet uint64
|
||||
WorkingSetPeak uint64
|
||||
WorkingSetPrivate uint64
|
||||
}
|
||||
|
||||
func (c *ProcessCollector) collect(ch chan<- prometheus.Metric) (*prometheus.Desc, error) {
|
||||
var dst []Win32_PerfRawData_PerfProc_Process
|
||||
q := wmi.CreateQuery(&dst, c.queryWhereClause)
|
||||
if err := wmi.Query(q, &dst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, process := range dst {
|
||||
|
||||
if process.Name == "_Total" {
|
||||
continue
|
||||
}
|
||||
// Duplicate processes are suffixed # and an index number. Remove those.
|
||||
processName := strings.Split(process.Name, "#")[0]
|
||||
pid := strconv.FormatUint(uint64(process.IDProcess), 10)
|
||||
cpid := strconv.FormatUint(uint64(process.CreatingProcessID), 10)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.StartTime,
|
||||
prometheus.GaugeValue,
|
||||
// convert from Windows timestamp (1 jan 1601) to unix timestamp (1 jan 1970)
|
||||
float64(process.ElapsedTime-116444736000000000)/float64(process.Frequency_Object),
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.HandleCount,
|
||||
prometheus.GaugeValue,
|
||||
float64(process.HandleCount),
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.CPUTimeTotal,
|
||||
prometheus.CounterValue,
|
||||
float64(process.PercentPrivilegedTime)*ticksToSecondsScaleFactor,
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
"privileged",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.CPUTimeTotal,
|
||||
prometheus.CounterValue,
|
||||
float64(process.PercentUserTime)*ticksToSecondsScaleFactor,
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
"user",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.IOBytesTotal,
|
||||
prometheus.CounterValue,
|
||||
float64(process.IOOtherBytesPersec),
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
"other",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.IOOperationsTotal,
|
||||
prometheus.CounterValue,
|
||||
float64(process.IOOtherOperationsPersec),
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
"other",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.IOBytesTotal,
|
||||
prometheus.CounterValue,
|
||||
float64(process.IOReadBytesPersec),
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
"read",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.IOOperationsTotal,
|
||||
prometheus.CounterValue,
|
||||
float64(process.IOReadOperationsPersec),
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
"read",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.IOBytesTotal,
|
||||
prometheus.CounterValue,
|
||||
float64(process.IOWriteBytesPersec),
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
"write",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.IOOperationsTotal,
|
||||
prometheus.CounterValue,
|
||||
float64(process.IOWriteOperationsPersec),
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
"write",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.PageFaultsTotal,
|
||||
prometheus.CounterValue,
|
||||
float64(process.PageFaultsPersec),
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.PageFileBytes,
|
||||
prometheus.GaugeValue,
|
||||
float64(process.PageFileBytes),
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.PoolBytes,
|
||||
prometheus.GaugeValue,
|
||||
float64(process.PoolNonpagedBytes),
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
"nonpaged",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.PoolBytes,
|
||||
prometheus.GaugeValue,
|
||||
float64(process.PoolPagedBytes),
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
"paged",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.PriorityBase,
|
||||
prometheus.GaugeValue,
|
||||
float64(process.PriorityBase),
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.PrivateBytes,
|
||||
prometheus.GaugeValue,
|
||||
float64(process.PrivateBytes),
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.ThreadCount,
|
||||
prometheus.GaugeValue,
|
||||
float64(process.ThreadCount),
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.VirtualBytes,
|
||||
prometheus.GaugeValue,
|
||||
float64(process.VirtualBytes),
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.WorkingSet,
|
||||
prometheus.GaugeValue,
|
||||
float64(process.WorkingSet),
|
||||
processName,
|
||||
pid,
|
||||
cpid,
|
||||
)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
@@ -14,15 +16,30 @@ func init() {
|
||||
Factories["service"] = NewserviceCollector
|
||||
}
|
||||
|
||||
var (
|
||||
serviceWhereClause = flag.String("collector.service.services-where", "", "WQL 'where' clause to use in WMI metrics query. Limits the response to the services you specify and reduces the size of the response.")
|
||||
)
|
||||
|
||||
// A serviceCollector is a Prometheus collector for WMI Win32_Service metrics
|
||||
type serviceCollector struct {
|
||||
State *prometheus.Desc
|
||||
StartMode *prometheus.Desc
|
||||
|
||||
queryWhereClause string
|
||||
}
|
||||
|
||||
// NewserviceCollector ...
|
||||
func NewserviceCollector() (Collector, error) {
|
||||
const subsystem = "service"
|
||||
|
||||
var wc bytes.Buffer
|
||||
if *serviceWhereClause != "" {
|
||||
wc.WriteString("WHERE ")
|
||||
wc.WriteString(*serviceWhereClause)
|
||||
} else {
|
||||
log.Println("warning: No where-clause specified for process collector. This will generate a very large number of metrics!")
|
||||
}
|
||||
|
||||
return &serviceCollector{
|
||||
State: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, subsystem, "state"),
|
||||
@@ -36,6 +53,7 @@ func NewserviceCollector() (Collector, error) {
|
||||
[]string{"name", "start_mode"},
|
||||
nil,
|
||||
),
|
||||
queryWhereClause: wc.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -77,7 +95,7 @@ var (
|
||||
|
||||
func (c *serviceCollector) collect(ch chan<- prometheus.Metric) (*prometheus.Desc, error) {
|
||||
var dst []Win32_Service
|
||||
q := wmi.CreateQuery(&dst, "")
|
||||
q := wmi.CreateQuery(&dst, c.queryWhereClause)
|
||||
if err := wmi.Query(q, &dst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
package collector
|
||||
|
||||
import "github.com/prometheus/client_golang/prometheus"
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// ...
|
||||
const (
|
||||
@@ -18,3 +24,30 @@ type Collector interface {
|
||||
// Get new metrics and expose them via prometheus registry.
|
||||
Collect(ch chan<- prometheus.Metric) (err error)
|
||||
}
|
||||
|
||||
// This is adapted from StackExchange/wmi/wmi.go, and lets us change the class
|
||||
// name being queried for:
|
||||
// CreateQuery returns a WQL query string that queries all columns of src. where
|
||||
// is an optional string that is appended to the query, to be used with WHERE
|
||||
// clauses. In such a case, the "WHERE" string should appear at the beginning.
|
||||
func createQuery(src interface{}, class, where string) string {
|
||||
var b bytes.Buffer
|
||||
b.WriteString("SELECT ")
|
||||
s := reflect.Indirect(reflect.ValueOf(src))
|
||||
t := s.Type()
|
||||
if s.Kind() == reflect.Slice {
|
||||
t = t.Elem()
|
||||
}
|
||||
if t.Kind() != reflect.Struct {
|
||||
return ""
|
||||
}
|
||||
var fields []string
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
fields = append(fields, t.Field(i).Name)
|
||||
}
|
||||
b.WriteString(strings.Join(fields, ", "))
|
||||
b.WriteString(" FROM ")
|
||||
b.WriteString(class)
|
||||
b.WriteString(" " + where)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
65
exporter.go
65
exporter.go
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"golang.org/x/sys/windows/svc"
|
||||
|
||||
"github.com/StackExchange/wmi"
|
||||
"github.com/martinlindhe/wmi_exporter/collector"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
@@ -32,21 +33,25 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
scrapeDurations = prometheus.NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Namespace: collector.Namespace,
|
||||
Subsystem: "exporter",
|
||||
Name: "scrape_duration_seconds",
|
||||
Help: "wmi_exporter: Duration of a scrape job.",
|
||||
},
|
||||
[]string{"collector", "result"},
|
||||
scrapeDurationDesc = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(collector.Namespace, "exporter", "collector_duration_seconds"),
|
||||
"wmi_exporter: Duration of a collection.",
|
||||
[]string{"collector"},
|
||||
nil,
|
||||
)
|
||||
scrapeSuccessDesc = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(collector.Namespace, "exporter", "collector_success"),
|
||||
"wmi_exporter: Whether the collector was successful.",
|
||||
[]string{"collector"},
|
||||
nil,
|
||||
)
|
||||
)
|
||||
|
||||
// Describe sends all the descriptors of the collectors included to
|
||||
// the provided channel.
|
||||
func (coll WmiCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
scrapeDurations.Describe(ch)
|
||||
ch <- scrapeDurationDesc
|
||||
ch <- scrapeSuccessDesc
|
||||
}
|
||||
|
||||
// Collect sends the collected metrics from each of the collectors to
|
||||
@@ -62,7 +67,6 @@ func (coll WmiCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
}(name, c)
|
||||
}
|
||||
wg.Wait()
|
||||
scrapeDurations.Collect(ch)
|
||||
}
|
||||
|
||||
func filterAvailableCollectors(collectors string) string {
|
||||
@@ -80,16 +84,27 @@ func execute(name string, c collector.Collector, ch chan<- prometheus.Metric) {
|
||||
begin := time.Now()
|
||||
err := c.Collect(ch)
|
||||
duration := time.Since(begin)
|
||||
var result string
|
||||
var success float64
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("ERROR: %s collector failed after %fs: %s", name, duration.Seconds(), err)
|
||||
result = "error"
|
||||
success = 0
|
||||
} else {
|
||||
log.Debugf("OK: %s collector succeeded after %fs.", name, duration.Seconds())
|
||||
result = "success"
|
||||
success = 1
|
||||
}
|
||||
scrapeDurations.WithLabelValues(name, result).Observe(duration.Seconds())
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
scrapeDurationDesc,
|
||||
prometheus.GaugeValue,
|
||||
duration.Seconds(),
|
||||
name,
|
||||
)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
scrapeSuccessDesc,
|
||||
prometheus.GaugeValue,
|
||||
success,
|
||||
name,
|
||||
)
|
||||
}
|
||||
|
||||
func expandEnabledCollectors(enabled string) []string {
|
||||
@@ -130,6 +145,25 @@ func init() {
|
||||
prometheus.MustRegister(version.NewCollector("wmi_exporter"))
|
||||
}
|
||||
|
||||
func initWbem() {
|
||||
// This initialization prevents a memory leak on WMF 5+. See
|
||||
// https://github.com/martinlindhe/wmi_exporter/issues/77 and linked issues
|
||||
// for details.
|
||||
log.Debugf("Initializing SWbemServices")
|
||||
s, err := wmi.InitializeSWbemServices(wmi.DefaultClient)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
wmi.DefaultClient.SWbemServicesClient = s
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintf(os.Stderr, "\nNote: If executing from Powershell, the flags need to quoted. For example:\n%s\n",
|
||||
"\twmi_exporter \"-collectors.enabled\" iis")
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
showVersion = flag.Bool("version", false, "Print version information.")
|
||||
@@ -138,6 +172,7 @@ func main() {
|
||||
enabledCollectors = flag.String("collectors.enabled", filterAvailableCollectors(defaultCollectors), "Comma-separated list of collectors to use. Use '[default]' as a placeholder for all the collectors enabled by default")
|
||||
printCollectors = flag.Bool("collectors.print", false, "If true, print available collectors and exit.")
|
||||
)
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
if *showVersion {
|
||||
@@ -158,6 +193,8 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
initWbem()
|
||||
|
||||
isInteractive, err := svc.IsAnInteractiveSession()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -42,10 +42,8 @@ function Get-FileIfNotExists {
|
||||
$sourceDir = mkdir -Force Source
|
||||
mkdir -Force Work,Output | Out-Null
|
||||
|
||||
Write-Verbose "Downloading files"
|
||||
# Somewhat obscure url, points to WiX 3.10 binary release
|
||||
Write-Verbose "Downloading WiX..."
|
||||
Get-FileIfNotExists "http://download-codeplex.sec.s-msft.com/Download/Release?ProjectName=wix&DownloadId=1587180&FileTime=131118854877130000&Build=21050" "$sourceDir\wix-binaries.zip"
|
||||
Get-FileIfNotExists "https://github.com/wixtoolset/wix3/releases/download/wix311rtm/wix311-binaries.zip" "$sourceDir\wix-binaries.zip"
|
||||
mkdir -Force WiX | Out-Null
|
||||
Expand-Archive -Path "${sourceDir}\wix-binaries.zip" -DestinationPath WiX -Force
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<File Id="wmi_exporter.exe" Name="wmi_exporter.exe" Source="Work\wmi_exporter.exe" KeyPath="yes">
|
||||
<fw:FirewallException Id="MetricsEndpoint" Name="WMI Exporter (HTTP [LISTEN_PORT])" Description="WMI Exporter HTTP endpoint" Port="[LISTEN_PORT]" Protocol="tcp" Scope="any" IgnoreFailure="yes" />
|
||||
</File>
|
||||
<ServiceInstall Id="InstallExporterService" Name="wmi_exporter" ErrorControl="normal" Start="auto" Type="ownProcess" Arguments="-log.format logger:eventlog?name=wmi_exporter [CollectorsFlag] [ListenFlag] [MetricsPathFlag]" />
|
||||
<ServiceInstall Id="InstallExporterService" Name="wmi_exporter" DisplayName="WMI exporter" Description="Exports Prometheus metrics from WMI queries" ErrorControl="normal" Start="auto" Type="ownProcess" Arguments="-log.format logger:eventlog?name=wmi_exporter [CollectorsFlag] [ListenFlag] [MetricsPathFlag]" />
|
||||
<ServiceControl Id="ServiceStateControl" Name="wmi_exporter" Remove="uninstall" Start="install" Stop="both" />
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
|
||||
4
vendor/github.com/StackExchange/wmi/README.md
generated
vendored
4
vendor/github.com/StackExchange/wmi/README.md
generated
vendored
@@ -1,4 +1,6 @@
|
||||
wmi
|
||||
===
|
||||
|
||||
Package wmi provides a WQL interface for WMI on Windows.
|
||||
Package wmi provides a WQL interface to Windows WMI.
|
||||
|
||||
Note: It interfaces with WMI on the local machine, therefore it only runs on Windows.
|
||||
|
||||
260
vendor/github.com/StackExchange/wmi/swbemservices.go
generated
vendored
Normal file
260
vendor/github.com/StackExchange/wmi/swbemservices.go
generated
vendored
Normal file
@@ -0,0 +1,260 @@
|
||||
// +build windows
|
||||
|
||||
package wmi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/go-ole/go-ole"
|
||||
"github.com/go-ole/go-ole/oleutil"
|
||||
)
|
||||
|
||||
// SWbemServices is used to access wmi. See https://msdn.microsoft.com/en-us/library/aa393719(v=vs.85).aspx
|
||||
type SWbemServices struct {
|
||||
//TODO: track namespace. Not sure if we can re connect to a different namespace using the same instance
|
||||
cWMIClient *Client //This could also be an embedded struct, but then we would need to branch on Client vs SWbemServices in the Query method
|
||||
sWbemLocatorIUnknown *ole.IUnknown
|
||||
sWbemLocatorIDispatch *ole.IDispatch
|
||||
queries chan *queryRequest
|
||||
closeError chan error
|
||||
lQueryorClose sync.Mutex
|
||||
}
|
||||
|
||||
type queryRequest struct {
|
||||
query string
|
||||
dst interface{}
|
||||
args []interface{}
|
||||
finished chan error
|
||||
}
|
||||
|
||||
// InitializeSWbemServices will return a new SWbemServices object that can be used to query WMI
|
||||
func InitializeSWbemServices(c *Client, connectServerArgs ...interface{}) (*SWbemServices, error) {
|
||||
//fmt.Println("InitializeSWbemServices: Starting")
|
||||
//TODO: implement connectServerArgs as optional argument for init with connectServer call
|
||||
s := new(SWbemServices)
|
||||
s.cWMIClient = c
|
||||
s.queries = make(chan *queryRequest)
|
||||
initError := make(chan error)
|
||||
go s.process(initError)
|
||||
|
||||
err, ok := <-initError
|
||||
if ok {
|
||||
return nil, err //Send error to caller
|
||||
}
|
||||
//fmt.Println("InitializeSWbemServices: Finished")
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Close will clear and release all of the SWbemServices resources
|
||||
func (s *SWbemServices) Close() error {
|
||||
s.lQueryorClose.Lock()
|
||||
if s == nil || s.sWbemLocatorIDispatch == nil {
|
||||
s.lQueryorClose.Unlock()
|
||||
return fmt.Errorf("SWbemServices is not Initialized")
|
||||
}
|
||||
if s.queries == nil {
|
||||
s.lQueryorClose.Unlock()
|
||||
return fmt.Errorf("SWbemServices has been closed")
|
||||
}
|
||||
//fmt.Println("Close: sending close request")
|
||||
var result error
|
||||
ce := make(chan error)
|
||||
s.closeError = ce //Race condition if multiple callers to close. May need to lock here
|
||||
close(s.queries) //Tell background to shut things down
|
||||
s.lQueryorClose.Unlock()
|
||||
err, ok := <-ce
|
||||
if ok {
|
||||
result = err
|
||||
}
|
||||
//fmt.Println("Close: finished")
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *SWbemServices) process(initError chan error) {
|
||||
//fmt.Println("process: starting background thread initialization")
|
||||
//All OLE/WMI calls must happen on the same initialized thead, so lock this goroutine
|
||||
runtime.LockOSThread()
|
||||
defer runtime.LockOSThread()
|
||||
|
||||
err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED)
|
||||
if err != nil {
|
||||
oleCode := err.(*ole.OleError).Code()
|
||||
if oleCode != ole.S_OK && oleCode != S_FALSE {
|
||||
initError <- fmt.Errorf("ole.CoInitializeEx error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
defer ole.CoUninitialize()
|
||||
|
||||
unknown, err := oleutil.CreateObject("WbemScripting.SWbemLocator")
|
||||
if err != nil {
|
||||
initError <- fmt.Errorf("CreateObject SWbemLocator error: %v", err)
|
||||
return
|
||||
} else if unknown == nil {
|
||||
initError <- ErrNilCreateObject
|
||||
return
|
||||
}
|
||||
defer unknown.Release()
|
||||
s.sWbemLocatorIUnknown = unknown
|
||||
|
||||
dispatch, err := s.sWbemLocatorIUnknown.QueryInterface(ole.IID_IDispatch)
|
||||
if err != nil {
|
||||
initError <- fmt.Errorf("SWbemLocator QueryInterface error: %v", err)
|
||||
return
|
||||
}
|
||||
defer dispatch.Release()
|
||||
s.sWbemLocatorIDispatch = dispatch
|
||||
|
||||
// we can't do the ConnectServer call outside the loop unless we find a way to track and re-init the connectServerArgs
|
||||
//fmt.Println("process: initialized. closing initError")
|
||||
close(initError)
|
||||
//fmt.Println("process: waiting for queries")
|
||||
for q := range s.queries {
|
||||
//fmt.Printf("process: new query: len(query)=%d\n", len(q.query))
|
||||
errQuery := s.queryBackground(q)
|
||||
//fmt.Println("process: s.queryBackground finished")
|
||||
if errQuery != nil {
|
||||
q.finished <- errQuery
|
||||
}
|
||||
close(q.finished)
|
||||
}
|
||||
//fmt.Println("process: queries channel closed")
|
||||
s.queries = nil //set channel to nil so we know it is closed
|
||||
//TODO: I think the Release/Clear calls can panic if things are in a bad state.
|
||||
//TODO: May need to recover from panics and send error to method caller instead.
|
||||
close(s.closeError)
|
||||
}
|
||||
|
||||
// Query runs the WQL query using a SWbemServices instance and appends the values to dst.
|
||||
//
|
||||
// dst must have type *[]S or *[]*S, for some struct type S. Fields selected in
|
||||
// the query must have the same name in dst. Supported types are all signed and
|
||||
// unsigned integers, time.Time, string, bool, or a pointer to one of those.
|
||||
// Array types are not supported.
|
||||
//
|
||||
// By default, the local machine and default namespace are used. These can be
|
||||
// changed using connectServerArgs. See
|
||||
// http://msdn.microsoft.com/en-us/library/aa393720.aspx for details.
|
||||
func (s *SWbemServices) Query(query string, dst interface{}, connectServerArgs ...interface{}) error {
|
||||
s.lQueryorClose.Lock()
|
||||
if s == nil || s.sWbemLocatorIDispatch == nil {
|
||||
s.lQueryorClose.Unlock()
|
||||
return fmt.Errorf("SWbemServices is not Initialized")
|
||||
}
|
||||
if s.queries == nil {
|
||||
s.lQueryorClose.Unlock()
|
||||
return fmt.Errorf("SWbemServices has been closed")
|
||||
}
|
||||
|
||||
//fmt.Println("Query: Sending query request")
|
||||
qr := queryRequest{
|
||||
query: query,
|
||||
dst: dst,
|
||||
args: connectServerArgs,
|
||||
finished: make(chan error),
|
||||
}
|
||||
s.queries <- &qr
|
||||
s.lQueryorClose.Unlock()
|
||||
err, ok := <-qr.finished
|
||||
if ok {
|
||||
//fmt.Println("Query: Finished with error")
|
||||
return err //Send error to caller
|
||||
}
|
||||
//fmt.Println("Query: Finished")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SWbemServices) queryBackground(q *queryRequest) error {
|
||||
if s == nil || s.sWbemLocatorIDispatch == nil {
|
||||
return fmt.Errorf("SWbemServices is not Initialized")
|
||||
}
|
||||
wmi := s.sWbemLocatorIDispatch //Should just rename in the code, but this will help as we break things apart
|
||||
//fmt.Println("queryBackground: Starting")
|
||||
|
||||
dv := reflect.ValueOf(q.dst)
|
||||
if dv.Kind() != reflect.Ptr || dv.IsNil() {
|
||||
return ErrInvalidEntityType
|
||||
}
|
||||
dv = dv.Elem()
|
||||
mat, elemType := checkMultiArg(dv)
|
||||
if mat == multiArgTypeInvalid {
|
||||
return ErrInvalidEntityType
|
||||
}
|
||||
|
||||
// service is a SWbemServices
|
||||
serviceRaw, err := oleutil.CallMethod(wmi, "ConnectServer", q.args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service := serviceRaw.ToIDispatch()
|
||||
defer serviceRaw.Clear()
|
||||
|
||||
// result is a SWBemObjectSet
|
||||
resultRaw, err := oleutil.CallMethod(service, "ExecQuery", q.query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result := resultRaw.ToIDispatch()
|
||||
defer resultRaw.Clear()
|
||||
|
||||
count, err := oleInt64(result, "Count")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
enumProperty, err := result.GetProperty("_NewEnum")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer enumProperty.Clear()
|
||||
|
||||
enum, err := enumProperty.ToIUnknown().IEnumVARIANT(ole.IID_IEnumVariant)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if enum == nil {
|
||||
return fmt.Errorf("can't get IEnumVARIANT, enum is nil")
|
||||
}
|
||||
defer enum.Release()
|
||||
|
||||
// Initialize a slice with Count capacity
|
||||
dv.Set(reflect.MakeSlice(dv.Type(), 0, int(count)))
|
||||
|
||||
var errFieldMismatch error
|
||||
for itemRaw, length, err := enum.Next(1); length > 0; itemRaw, length, err = enum.Next(1) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := func() error {
|
||||
// item is a SWbemObject, but really a Win32_Process
|
||||
item := itemRaw.ToIDispatch()
|
||||
defer item.Release()
|
||||
|
||||
ev := reflect.New(elemType)
|
||||
if err = s.cWMIClient.loadEntity(ev.Interface(), item); err != nil {
|
||||
if _, ok := err.(*ErrFieldMismatch); ok {
|
||||
// We continue loading entities even in the face of field mismatch errors.
|
||||
// If we encounter any other error, that other error is returned. Otherwise,
|
||||
// an ErrFieldMismatch is returned.
|
||||
errFieldMismatch = err
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if mat != multiArgTypeStructPtr {
|
||||
ev = ev.Elem()
|
||||
}
|
||||
dv.Set(reflect.Append(dv, ev))
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
//fmt.Println("queryBackground: Finished")
|
||||
return errFieldMismatch
|
||||
}
|
||||
25
vendor/github.com/StackExchange/wmi/wmi.go
generated
vendored
25
vendor/github.com/StackExchange/wmi/wmi.go
generated
vendored
@@ -72,7 +72,10 @@ func QueryNamespace(query string, dst interface{}, namespace string) error {
|
||||
//
|
||||
// Query is a wrapper around DefaultClient.Query.
|
||||
func Query(query string, dst interface{}, connectServerArgs ...interface{}) error {
|
||||
return DefaultClient.Query(query, dst, connectServerArgs...)
|
||||
if DefaultClient.SWbemServicesClient == nil {
|
||||
return DefaultClient.Query(query, dst, connectServerArgs...)
|
||||
}
|
||||
return DefaultClient.SWbemServicesClient.Query(query, dst, connectServerArgs...)
|
||||
}
|
||||
|
||||
// A Client is an WMI query client.
|
||||
@@ -99,6 +102,11 @@ type Client struct {
|
||||
// Setting this to true allows custom queries to be used with full
|
||||
// struct definitions instead of having to define multiple structs.
|
||||
AllowMissingFields bool
|
||||
|
||||
// SWbemServiceClient is an optional SWbemServices object that can be
|
||||
// initialized and then reused across multiple queries. If it is null
|
||||
// then the method will initialize a new temporary client each time.
|
||||
SWbemServicesClient *SWbemServices
|
||||
}
|
||||
|
||||
// DefaultClient is the default Client and is used by Query, QueryNamespace
|
||||
@@ -362,6 +370,21 @@ func (c *Client) loadEntity(dst interface{}, src *ole.IDispatch) (errFieldMismat
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Only support []string slices for now
|
||||
if f.Kind() == reflect.Slice && f.Type().Elem().Kind() == reflect.String {
|
||||
safeArray := prop.ToArray()
|
||||
if safeArray != nil {
|
||||
arr := safeArray.ToValueArray()
|
||||
fArr := reflect.MakeSlice(f.Type(), len(arr), len(arr))
|
||||
for i, v := range arr {
|
||||
s := fArr.Index(i)
|
||||
s.SetString(v.(string))
|
||||
}
|
||||
f.Set(fArr)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
typeof := reflect.TypeOf(val)
|
||||
if typeof == nil && (isPtr || c.NonePtrZero) {
|
||||
if (isPtr && c.PtrNil) || (!isPtr && c.NonePtrZero) {
|
||||
|
||||
12
vendor/github.com/go-ole/go-ole/variant_s390x.go
generated
vendored
Normal file
12
vendor/github.com/go-ole/go-ole/variant_s390x.go
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
// +build s390x
|
||||
|
||||
package ole
|
||||
|
||||
type VARIANT struct {
|
||||
VT VT // 2
|
||||
wReserved1 uint16 // 4
|
||||
wReserved2 uint16 // 6
|
||||
wReserved3 uint16 // 8
|
||||
Val int64 // 16
|
||||
_ [8]byte // 24
|
||||
}
|
||||
12
vendor/vendor.json
vendored
12
vendor/vendor.json
vendored
@@ -9,10 +9,10 @@
|
||||
"revisionTime": "2016-08-29T20:23:21Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "9NR0rrcAT5J76C5xMS4AVksS9o0=",
|
||||
"checksumSHA1": "qtjd74+bErubh+qyv3s+lWmn9wc=",
|
||||
"path": "github.com/StackExchange/wmi",
|
||||
"revision": "e54cbda6595d7293a7a468ccf9525f6bc8887f99",
|
||||
"revisionTime": "2016-08-11T21:45:55Z"
|
||||
"revision": "ea383cf3ba6ec950874b8486cd72356d007c768f",
|
||||
"revisionTime": "2017-04-10T19:29:09Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "spyv5/YFBjYyZLZa1U2LBfDR8PM=",
|
||||
@@ -21,10 +21,10 @@
|
||||
"revisionTime": "2016-08-04T10:47:26Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "wDZdTaY9JiqqqnF4c3pHP71nWmk=",
|
||||
"checksumSHA1": "oyULO1B/l2OLy2xereo+2w3hYk4=",
|
||||
"path": "github.com/go-ole/go-ole",
|
||||
"revision": "7dfdcf409020452e29b4babcbb22f984d2aa308a",
|
||||
"revisionTime": "2016-07-29T03:38:29Z"
|
||||
"revision": "02d3668a0cf01f58411cc85cd37c174c257ec7c2",
|
||||
"revisionTime": "2017-06-01T13:56:11Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "qLYVTQDhgrVIeZ2KI9eZV51mmug=",
|
||||
|
||||
Reference in New Issue
Block a user