diff --git a/config/hostname.go b/config/hostname.go index 8091fd0f..b3e6891c 100644 --- a/config/hostname.go +++ b/config/hostname.go @@ -56,9 +56,13 @@ func InitHostInfo() error { return err } - ip, err := GetOutboundIP() - if err != nil { - return err + var ip string + if ip = os.Getenv("HOSTIP"); ip == "" { + nip, err := GetOutboundIP() + if err != nil { + return err + } + ip = fmt.Sprint(nip) } HostInfo = &HostInfoCache{ diff --git a/heartbeat/cpu/cpu.go b/heartbeat/cpu/cpu.go new file mode 100644 index 00000000..48c2bbc7 --- /dev/null +++ b/heartbeat/cpu/cpu.go @@ -0,0 +1,19 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package cpu + +type Cpu struct{} + +const name = "cpu" + +func (self *Cpu) Name() string { + return name +} + +func (self *Cpu) Collect() (result interface{}, err error) { + result, err = getCpuInfo() + return +} diff --git a/heartbeat/cpu/cpu_darwin.go b/heartbeat/cpu/cpu_darwin.go new file mode 100644 index 00000000..463a65a2 --- /dev/null +++ b/heartbeat/cpu/cpu_darwin.go @@ -0,0 +1,44 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package cpu + +import ( + "os/exec" + "strconv" + "strings" +) + +var cpuMap = map[string]string{ + "machdep.cpu.vendor": "vendor_id", + "machdep.cpu.brand_string": "model_name", + "hw.physicalcpu": "cpu_cores", + "hw.logicalcpu": "cpu_logical_processors", + "hw.cpufrequency": "mhz", + "machdep.cpu.family": "family", + "machdep.cpu.model": "model", + "machdep.cpu.stepping": "stepping", +} + +func getCpuInfo() (cpuInfo map[string]string, err error) { + + cpuInfo = make(map[string]string) + + for option, key := range cpuMap { + out, err := exec.Command("sysctl", "-n", option).Output() + if err == nil { + cpuInfo[key] = strings.Trim(string(out), "\n") + } + } + + if len(cpuInfo["mhz"]) != 0 { + mhz, err := strconv.Atoi(cpuInfo["mhz"]) + if err == nil { + cpuInfo["mhz"] = strconv.Itoa(mhz / 1000000) + } + } + + return +} diff --git a/heartbeat/cpu/cpu_linux.go b/heartbeat/cpu/cpu_linux.go new file mode 100644 index 00000000..ae602083 --- /dev/null +++ b/heartbeat/cpu/cpu_linux.go @@ -0,0 +1,90 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package cpu + +import ( + "bufio" + "os" + "regexp" + "strconv" +) + +var cpuMap = map[string]string{ + "vendor_id": "vendor_id", + "model name": "model_name", + "cpu cores": "cpu_cores", + "siblings": "cpu_logical_processors", + "cpu MHz\t": "mhz", + "cache size": "cache_size", + "cpu family": "family", + "model\t": "model", + "stepping": "stepping", +} + +// Values that need to be multiplied by the number of physical processors +var perPhysicalProcValues = []string{ + "cpu_cores", + "cpu_logical_processors", +} + +func getCpuInfo() (cpuInfo map[string]string, err error) { + lines, err := readProcFile() + if err != nil { + return + } + + cpuInfo = make(map[string]string) + // Implementation of a set that holds the physical IDs + physicalProcIDs := make(map[string]struct{}) + + for _, line := range lines { + pair := regexp.MustCompile("\t: ").Split(line, 2) + + if pair[0] == "physical id" { + physicalProcIDs[pair[1]] = struct{}{} + } + + key, ok := cpuMap[pair[0]] + if ok { + cpuInfo[key] = pair[1] + } + } + + // Multiply the values that are "per physical processor" by the number of physical procs + for _, field := range perPhysicalProcValues { + if value, ok := cpuInfo[field]; ok { + intValue, err := strconv.Atoi(value) + if err != nil { + continue + } + + cpuInfo[field] = strconv.Itoa(intValue * len(physicalProcIDs)) + } + } + + return +} + +func readProcFile() (lines []string, err error) { + file, err := os.Open("/proc/cpuinfo") + + if err != nil { + return + } + + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + if scanner.Err() != nil { + err = scanner.Err() + return + } + + return +} diff --git a/heartbeat/cpu/cpu_windows.go b/heartbeat/cpu/cpu_windows.go new file mode 100644 index 00000000..efac3bad --- /dev/null +++ b/heartbeat/cpu/cpu_windows.go @@ -0,0 +1,192 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package cpu + +import ( + "fmt" + "regexp" + "strconv" + "strings" + "syscall" + "unsafe" + + "golang.org/x/sys/windows/registry" +) + +var getCpuInfo = GetCpuInfo + +// Values that need to be multiplied by the number of physical processors +var perPhysicalProcValues = []string{ + "cpu_cores", + "cpu_logical_processors", +} + +const ERROR_INSUFFICIENT_BUFFER syscall.Errno = 122 +const registryHive = "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0" + +type CACHE_DESCRIPTOR struct { + Level uint8 + Associativity uint8 + LineSize uint16 + Size uint32 + cacheType uint32 +} +type SYSTEM_LOGICAL_PROCESSOR_INFORMATION struct { + ProcessorMask uintptr + Relationship int // enum (int) + // in the Windows header, this is a union of a byte, a DWORD, + // and a CACHE_DESCRIPTOR structure + dataunion [16]byte +} + +// .const SYSTEM_LOGICAL_PROCESSOR_INFORMATION_SIZE = 32 + +type GROUP_AFFINITY struct { + Mask uintptr + Group uint16 + Reserved [3]uint16 +} +type NUMA_NODE_RELATIONSHIP struct { + NodeNumber uint32 + Reserved [20]uint8 + GroupMask GROUP_AFFINITY +} +type CACHE_RELATIONSHIP struct { + Level uint8 + Associativity uint8 + LineSize uint16 + CacheSize uint32 + CacheType int // enum in C + Reserved [20]uint8 + GroupMask GROUP_AFFINITY +} + +type PROCESSOR_GROUP_INFO struct { + MaximumProcessorCount uint8 + ActiveProcessorCount uint8 + Reserved [38]uint8 + ActiveProcessorMask uintptr +} +type GROUP_RELATIONSHIP struct { + MaximumGroupCount uint16 + ActiveGroupCount uint16 + Reserved [20]uint8 + // variable size array of PROCESSOR_GROUP_INFO +} +type PROCESSOR_RELATIONSHIP struct { + Flags uint8 + EfficiencyClass uint8 + wReserved [20]uint8 + GroupCount uint16 + // what follows is an array of zero or more GROUP_AFFINITY structures +} + +type SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX struct { + Relationship int + Size uint32 + // what follows is a C union of + // PROCESSOR_RELATIONSHIP, + // NUMA_NODE_RELATIONSHIP, + // CACHE_RELATIONSHIP, + // GROUP_RELATIONSHIP +} + +const RelationProcessorCore = 0 +const RelationNumaNode = 1 +const RelationCache = 2 +const RelationProcessorPackage = 3 +const RelationGroup = 4 + +type SYSTEM_INFO struct { + wProcessorArchitecture uint16 + wReserved uint16 + dwPageSize uint32 + lpMinApplicationAddress *uint32 + lpMaxApplicationAddress *uint32 + dwActiveProcessorMask uintptr + dwNumberOfProcessors uint32 + dwProcessorType uint32 + dwAllocationGranularity uint32 + wProcessorLevel uint16 + wProcessorRevision uint16 +} + +type CPU_INFO struct { + numaNodeCount int // number of NUMA nodes + pkgcount int // number of packages (physical CPUS) + corecount int // total number of cores + logicalcount int // number of logical CPUS + l1CacheSize uint32 // layer 1 cache size + l2CacheSize uint32 // layer 2 cache size + l3CacheSize uint32 // layer 3 cache size + relationGroups int // number of cpu relation groups + maxProcsInGroups int // max number of processors + activeProcsInGroups int // active processors + +} + +func countBits(num uint64) (count int) { + count = 0 + for num > 0 { + if (num & 0x1) == 1 { + count++ + } + num >>= 1 + } + return +} + +func getSystemInfo() (si SYSTEM_INFO) { + var mod = syscall.NewLazyDLL("kernel32.dll") + var gsi = mod.NewProc("GetSystemInfo") + + gsi.Call(uintptr(unsafe.Pointer(&si))) + return +} + +// GetCpuInfo returns map of interesting bits of information about the CPU +func GetCpuInfo() (cpuInfo map[string]string, err error) { + + cpuInfo = make(map[string]string) + + cpus, _ := computeCoresAndProcessors() + si := getSystemInfo() + + k, err := registry.OpenKey(registry.LOCAL_MACHINE, + registryHive, + registry.QUERY_VALUE) + defer k.Close() + dw, _, err := k.GetIntegerValue("~MHz") + cpuInfo["mhz"] = strconv.Itoa(int(dw)) + + s, _, err := k.GetStringValue("ProcessorNameString") + cpuInfo["model_name"] = s + + cpuInfo["cpu_pkgs"] = strconv.Itoa(cpus.pkgcount) + cpuInfo["cpu_numa_nodes"] = strconv.Itoa(cpus.numaNodeCount) + cpuInfo["cpu_cores"] = strconv.Itoa(cpus.corecount) + cpuInfo["cpu_logical_processors"] = strconv.Itoa(cpus.logicalcount) + + s, _, err = k.GetStringValue("VendorIdentifier") + cpuInfo["vendor_id"] = s + + s, _, err = k.GetStringValue("Identifier") + cpuInfo["family"] = extract(s, "Family") + + cpuInfo["model"] = strconv.Itoa(int((si.wProcessorRevision >> 8) & 0xFF)) + cpuInfo["stepping"] = strconv.Itoa(int(si.wProcessorRevision & 0xFF)) + + cpuInfo["cache_size_l1"] = strconv.Itoa(int(cpus.l1CacheSize)) + cpuInfo["cache_size_l2"] = strconv.Itoa(int(cpus.l2CacheSize)) + cpuInfo["cache_size_l3"] = strconv.Itoa(int(cpus.l3CacheSize)) + + return +} + +func extract(caption, field string) string { + re := regexp.MustCompile(fmt.Sprintf("%s [0-9]* ", field)) + return strings.Split(re.FindStringSubmatch(caption)[0], " ")[1] +} diff --git a/heartbeat/cpu/cpu_windows_386.go b/heartbeat/cpu/cpu_windows_386.go new file mode 100644 index 00000000..aafea7df --- /dev/null +++ b/heartbeat/cpu/cpu_windows_386.go @@ -0,0 +1,70 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package cpu + +import ( + "encoding/binary" + "syscall" + "unsafe" +) + +const SYSTEM_LOGICAL_PROCESSOR_INFORMATION_SIZE = 24 + +func getSystemLogicalProcessorInformationSize() int { + return SYSTEM_LOGICAL_PROCESSOR_INFORMATION_SIZE +} +func byteArrayToProcessorStruct(data []byte) (info SYSTEM_LOGICAL_PROCESSOR_INFORMATION) { + info.ProcessorMask = uintptr(binary.LittleEndian.Uint32(data)) + info.Relationship = int(binary.LittleEndian.Uint32(data[4:])) + copy(info.dataunion[0:16], data[8:24]) + return +} + +func computeCoresAndProcessors() (cpuInfo CPU_INFO, err error) { + var mod = syscall.NewLazyDLL("kernel32.dll") + var getProcInfo = mod.NewProc("GetLogicalProcessorInformation") + var buflen uint32 = 0 + err = syscall.Errno(0) + // first, figure out how much we need + status, _, err := getProcInfo.Call(uintptr(0), + uintptr(unsafe.Pointer(&buflen))) + if status == 0 { + if err != ERROR_INSUFFICIENT_BUFFER { + // only error we're expecing here is insufficient buffer + // anything else is a failure + return + } + } else { + // this shouldn't happen. Errno won't be set (because the function) + // succeeded. So just return something to indicate we've failed + err = syscall.Errno(2) + return + } + buf := make([]byte, buflen) + status, _, err = getProcInfo.Call(uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&buflen))) + if status == 0 { + return + } + // walk through each of the buffers + + for i := 0; uint32(i) < buflen; i += getSystemLogicalProcessorInformationSize() { + info := byteArrayToProcessorStruct(buf[i : i+getSystemLogicalProcessorInformationSize()]) + + switch info.Relationship { + case RelationNumaNode: + cpuInfo.numaNodeCount++ + + case RelationProcessorCore: + cpuInfo.corecount++ + cpuInfo.logicalcount += countBits(uint64(info.ProcessorMask)) + + case RelationProcessorPackage: + cpuInfo.pkgcount++ + } + } + return +} diff --git a/heartbeat/cpu/cpu_windows_amd64.go b/heartbeat/cpu/cpu_windows_amd64.go new file mode 100644 index 00000000..364eb1bc --- /dev/null +++ b/heartbeat/cpu/cpu_windows_amd64.go @@ -0,0 +1,218 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package cpu + +import ( + "encoding/binary" + "syscall" + "unsafe" +) + +const SYSTEM_LOGICAL_PROCESSOR_INFORMATION_SIZE = 32 + +func getSystemLogicalProcessorInformationSize() int { + return SYSTEM_LOGICAL_PROCESSOR_INFORMATION_SIZE +} +func byteArrayToProcessorStruct(data []byte) (info SYSTEM_LOGICAL_PROCESSOR_INFORMATION) { + info.ProcessorMask = uintptr(binary.LittleEndian.Uint64(data)) + info.Relationship = int(binary.LittleEndian.Uint64(data[8:])) + copy(info.dataunion[0:16], data[16:32]) + return +} + +func byteArrayToGroupAffinity(data []byte) (affinity GROUP_AFFINITY, consumed uint32, err error) { + err = nil + affinity.Mask = uintptr(binary.LittleEndian.Uint64(data)) + affinity.Group = uint16(binary.LittleEndian.Uint16(data[8:])) + // can skip the reserved, but count it + consumed = 16 + return + +} +func byteArrayToProcessorInformationExStruct(data []byte) (info SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX, consumed uint32, err error) { + err = nil + info.Relationship = int(binary.LittleEndian.Uint32(data)) + info.Size = uint32(binary.LittleEndian.Uint32(data[4:])) + + consumed = 8 + return +} + +func byteArrayToProcessorRelationshipStruct(data []byte) (proc PROCESSOR_RELATIONSHIP, groupMask []GROUP_AFFINITY, consumed uint32, err error) { + err = nil + proc.Flags = uint8(data[0]) + proc.EfficiencyClass = uint8(data[1]) + proc.GroupCount = uint16(binary.LittleEndian.Uint32(data[22:])) + consumed = 24 + if proc.GroupCount != 0 { + gm := make([]GROUP_AFFINITY, proc.GroupCount) + + for i := uint16(0); i < proc.GroupCount; i++ { + var used uint32 + var ga GROUP_AFFINITY + ga, used, err = byteArrayToGroupAffinity(data[consumed:]) + if err != nil { + return + } + gm[i] = ga + consumed += used + } + groupMask = gm + } + return +} + +func byteArrayToNumaNode(data []byte) (numa NUMA_NODE_RELATIONSHIP, consumed uint32, err error) { + err = nil + numa.NodeNumber = uint32(binary.LittleEndian.Uint32(data)) + // skip 20 bytes of reserved + consumed = 24 + aff, used, err := byteArrayToGroupAffinity(data[consumed:]) + numa.GroupMask = aff + consumed += used + return +} + +func byteArrayToRelationCache(data []byte) (cache CACHE_RELATIONSHIP, consumed uint32, err error) { + cache.Level = uint8(data[0]) + cache.Associativity = uint8(data[1]) + cache.LineSize = uint16(binary.LittleEndian.Uint16(data[2:])) + cache.CacheSize = uint32(binary.LittleEndian.Uint32(data[4:])) + cache.CacheType = int(binary.LittleEndian.Uint32(data[8:])) + // skip 20 bytes + consumed = 32 + ga, used, err := byteArrayToGroupAffinity(data[consumed:]) + cache.GroupMask = ga + consumed += used + return + +} + +func byteArrayToRelationGroup(data []byte) (group GROUP_RELATIONSHIP, gi []PROCESSOR_GROUP_INFO, consumed uint32, err error) { + group.MaximumGroupCount = uint16(binary.LittleEndian.Uint16(data)) + group.ActiveGroupCount = uint16(binary.LittleEndian.Uint16(data[4:])) + consumed = 24 + if group.ActiveGroupCount > 0 { + groups := make([]PROCESSOR_GROUP_INFO, group.ActiveGroupCount) + for i := uint16(0); i < group.ActiveGroupCount; i++ { + groups[i].MaximumProcessorCount = uint8(data[consumed]) + consumed += 1 + groups[i].ActiveProcessorCount = uint8(data[consumed]) + consumed += 1 + consumed += 38 // reserved + groups[i].ActiveProcessorMask = uintptr(binary.LittleEndian.Uint64(data[consumed:])) + consumed += 8 + } + } + return +} + +func computeCoresAndProcessors() (cpuInfo CPU_INFO, err error) { + var mod = syscall.NewLazyDLL("kernel32.dll") + var getProcInfo = mod.NewProc("GetLogicalProcessorInformationEx") + var buflen uint32 = 0 + + err = syscall.Errno(0) + // first, figure out how much we need + status, _, err := getProcInfo.Call(uintptr(0xFFFF), // all relationships. + uintptr(0), + uintptr(unsafe.Pointer(&buflen))) + if status == 0 { + if err != ERROR_INSUFFICIENT_BUFFER { + // only error we're expecing here is insufficient buffer + // anything else is a failure + return + } + } else { + // this shouldn't happen. Errno won't be set (because the function) + // succeeded. So just return something to indicate we've failed + err = syscall.Errno(1) + return + } + buf := make([]byte, buflen) + status, _, err = getProcInfo.Call( + uintptr(0xFFFF), // still want all relationships + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&buflen))) + if status == 0 { + return + } + // walk through each of the buffers + + bufused := uint32(0) + for bufused < buflen { + info, used, decodeerr := byteArrayToProcessorInformationExStruct(buf[bufused:]) + if decodeerr != nil { + err = decodeerr + return + } + bufused += used + if info.Size == 0 { + break + } + switch info.Relationship { + case RelationProcessorCore: + core, groupMask, used, decodeerr := byteArrayToProcessorRelationshipStruct(buf[bufused:]) + if decodeerr != nil { + err = decodeerr + return + } + bufused += used + cpuInfo.corecount++ + for j := uint16(0); j < core.GroupCount; j++ { + cpuInfo.logicalcount += countBits(uint64(groupMask[j].Mask)) + } + case RelationNumaNode: + _, used, decodeerr := byteArrayToNumaNode(buf[bufused:]) + if decodeerr != nil { + err = decodeerr + return + } + cpuInfo.numaNodeCount++ + bufused += used + + case RelationCache: + cache, used, decodeerr := byteArrayToRelationCache(buf[bufused:]) + if decodeerr != nil { + err = decodeerr + return + } + bufused += used + switch cache.Level { + case 1: + cpuInfo.l1CacheSize = cache.CacheSize + case 2: + cpuInfo.l2CacheSize = cache.CacheSize + case 3: + cpuInfo.l3CacheSize = cache.CacheSize + } + case RelationProcessorPackage: + _, _, used, decodeerr := byteArrayToProcessorRelationshipStruct(buf[bufused:]) + if decodeerr != nil { + err = decodeerr + return + } + bufused += used + cpuInfo.pkgcount++ + + case RelationGroup: + group, groupInfo, used, decodeerr := byteArrayToRelationGroup(buf[bufused:]) + if decodeerr != nil { + err = decodeerr + return + } + bufused += used + cpuInfo.relationGroups += int(group.MaximumGroupCount) + for _, info := range groupInfo { + cpuInfo.maxProcsInGroups += int(info.MaximumProcessorCount) + cpuInfo.activeProcsInGroups += int(info.ActiveProcessorCount) + } + + } + } + + return +} diff --git a/heartbeat/cpu/cpu_windows_arm64.go b/heartbeat/cpu/cpu_windows_arm64.go new file mode 100644 index 00000000..c6231394 --- /dev/null +++ b/heartbeat/cpu/cpu_windows_arm64.go @@ -0,0 +1,10 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package cpu + +func computeCoresAndProcessors() (cpuInfo CPU_INFO, err error) { + return CPU_INFO{}, nil +} diff --git a/heartbeat/filesystem/filesystem.go b/heartbeat/filesystem/filesystem.go new file mode 100644 index 00000000..e662dfb3 --- /dev/null +++ b/heartbeat/filesystem/filesystem.go @@ -0,0 +1,48 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +//go:build linux || darwin +// +build linux darwin + +package filesystem + +import ( + "context" + "fmt" + "os/exec" + "regexp" + "strings" + "time" +) + +func getFileSystemInfo() (interface{}, error) { + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + /* Grab filesystem data from df */ + cmd := exec.CommandContext(ctx, "df", dfOptions...) + + out, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("df failed to collect filesystem data: %s", err) + } + if out != nil { + return parseDfOutput(string(out)) + } + return nil, fmt.Errorf("df failed to collect filesystem data") +} + +func parseDfOutput(out string) (interface{}, error) { + lines := strings.Split(out, "\n") + var fileSystemInfo = make([]interface{}, len(lines)-2) + for i, line := range lines[1:] { + values := regexp.MustCompile("\\s+").Split(line, expectedLength) + if len(values) == expectedLength { + fileSystemInfo[i] = updatefileSystemInfo(values) + } + } + return fileSystemInfo, nil +} diff --git a/heartbeat/filesystem/filesystem_common.go b/heartbeat/filesystem/filesystem_common.go new file mode 100644 index 00000000..f0cd6dce --- /dev/null +++ b/heartbeat/filesystem/filesystem_common.go @@ -0,0 +1,19 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package filesystem + +type FileSystem struct{} + +const name = "filesystem" + +func (self *FileSystem) Name() string { + return name +} + +func (self *FileSystem) Collect() (result interface{}, err error) { + result, err = getFileSystemInfo() + return +} diff --git a/heartbeat/filesystem/filesystem_darwin.go b/heartbeat/filesystem/filesystem_darwin.go new file mode 100644 index 00000000..3310dffb --- /dev/null +++ b/heartbeat/filesystem/filesystem_darwin.go @@ -0,0 +1,17 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package filesystem + +var dfOptions = []string{"-l", "-k"} +var expectedLength = 9 + +func updatefileSystemInfo(values []string) map[string]string { + return map[string]string{ + "name": values[0], + "kb_size": values[1], + "mounted_on": values[8], + } +} diff --git a/heartbeat/filesystem/filesystem_linux.go b/heartbeat/filesystem/filesystem_linux.go new file mode 100644 index 00000000..3c4c56c4 --- /dev/null +++ b/heartbeat/filesystem/filesystem_linux.go @@ -0,0 +1,17 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package filesystem + +var dfOptions = []string{"-l"} +var expectedLength = 6 + +func updatefileSystemInfo(values []string) map[string]string { + return map[string]string{ + "name": values[0], + "kb_size": values[1], + "mounted_on": values[5], + } +} diff --git a/heartbeat/filesystem/filesystem_windows.go b/heartbeat/filesystem/filesystem_windows.go new file mode 100644 index 00000000..526298db --- /dev/null +++ b/heartbeat/filesystem/filesystem_windows.go @@ -0,0 +1,140 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package filesystem + +import ( + "strconv" + "syscall" + "unsafe" +) + +type Handle uintptr + +const InvalidHandle Handle = Handle(^Handle(0)) +const ERROR_moreData syscall.Errno = 234 + +// this would probably go in a common utilities rather than here + +func convert_windows_string_list(winput []uint16) []string { + var retstrings []string + var rsindex = 0 + + retstrings = append(retstrings, "") + for i := 0; i < (len(winput) - 1); i++ { + if winput[i] == 0 { + if winput[i+1] == 0 { + return retstrings + } + rsindex++ + retstrings = append(retstrings, "") + continue + } + retstrings[rsindex] += string(rune(winput[i])) + } + return retstrings +} + +// as would this +func convert_windows_string(winput []uint16) string { + var retstring string + for i := 0; i < len(winput); i++ { + if winput[i] == 0 { + break + } + retstring += string(rune(winput[i])) + } + return retstring +} +func getDiskSize(vol string) (size uint64, freespace uint64) { + var mod = syscall.NewLazyDLL("kernel32.dll") + var getDisk = mod.NewProc("GetDiskFreeSpaceExW") + var sz uint64 + var fr uint64 + status, _, _ := getDisk.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(vol))), + uintptr(0), + uintptr(unsafe.Pointer(&sz)), + uintptr(unsafe.Pointer(&fr))) + if status == 0 { + return 0, 0 + } + return sz, fr +} +func getMountPoints(vol string) []string { + var mod = syscall.NewLazyDLL("kernel32.dll") + var getPaths = mod.NewProc("GetVolumePathNamesForVolumeNameW") + var tmp uint32 + var objlistsize uint32 = 0x0 + var retval []string + + status, _, errno := getPaths.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(vol))), + uintptr(unsafe.Pointer(&tmp)), + 2, + uintptr(unsafe.Pointer(&objlistsize))) + + if status != 0 || errno != ERROR_moreData { + // unexpected + return retval + } + buf := make([]uint16, objlistsize) + status, _, errno = getPaths.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(vol))), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(objlistsize), + uintptr(unsafe.Pointer(&objlistsize))) + if status == 0 { + return retval + } + return convert_windows_string_list(buf) + +} +func getFileSystemInfo() (interface{}, error) { + var mod = syscall.NewLazyDLL("kernel32.dll") + var findFirst = mod.NewProc("FindFirstVolumeW") + var findNext = mod.NewProc("FindNextVolumeW") + var findClose = mod.NewProc("FindVolumeClose") + + // var findHandle Handle + buf := make([]uint16, 512) + var sz int32 = 512 + fh, _, _ := findFirst.Call(uintptr(unsafe.Pointer(&buf[0])), + uintptr(sz)) + var findHandle Handle = Handle(fh) + var fileSystemInfo []interface{} + + if findHandle != InvalidHandle { + defer findClose.Call(fh) + moreData := true + for moreData { + outstring := convert_windows_string(buf) + sz, _ := getDiskSize(outstring) + var capacity string + if 0 == sz { + capacity = "Unknown" + } else { + capacity = strconv.FormatInt(int64(sz)/1024.0, 10) + } + mountpts := getMountPoints(outstring) + var mountName string + if len(mountpts) > 0 { + mountName = mountpts[0] + } + iface := map[string]interface{}{ + "name": outstring, + "kb_size": capacity, + "mounted_on": mountName, + } + fileSystemInfo = append(fileSystemInfo, iface) + status, _, _ := findNext.Call(uintptr(fh), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(sz)) + if 0 == status { + moreData = false + } + } + + } + + return fileSystemInfo, nil +} diff --git a/heartbeat/heartbeat.go b/heartbeat/heartbeat.go index 91a07245..e703e340 100644 --- a/heartbeat/heartbeat.go +++ b/heartbeat/heartbeat.go @@ -26,12 +26,6 @@ func Work() { return } - version := config.Version - versions := strings.Split(version, "-") - if len(versions) > 1 { - version = versions[0] - } - ps := system.NewSystemPS() interval := conf.Interval @@ -48,7 +42,7 @@ func Work() { duration := time.Second * time.Duration(interval-collinterval) for { - work(version, ps, client) + work(ps, client) time.Sleep(duration) } } @@ -88,13 +82,26 @@ func newHTTPClient() (*http.Client, error) { return client, nil } -func work(version string, ps *system.SystemPS, client *http.Client) { +func version() string { + components := strings.Split(config.Version, "-") + switch len(components) { + case 2: + return components[0] + case 3, 4: + return components[0] + "-" + components[1] + } + return config.Version +} + +func work(ps *system.SystemPS, client *http.Client) { cpuUsagePercent := cpuUsage(ps) hostname := config.Config.GetHostname() memUsagePercent := memUsage(ps) + shortVersion := version() + hostIP := config.Config.GetHostIP() data := map[string]interface{}{ - "agent_version": version, + "agent_version": shortVersion, "os": runtime.GOOS, "arch": runtime.GOARCH, "hostname": hostname, @@ -102,6 +109,13 @@ func work(version string, ps *system.SystemPS, client *http.Client) { "cpu_util": cpuUsagePercent, "mem_util": memUsagePercent, "unixtime": time.Now().UnixMilli(), + "host_ip": hostIP, + } + + if ext, err := collectSystemInfo(); err == nil { + data["extend_info"] = ext + } else { + log.Println("E! failed to collect system info:", err) } bs, err := json.Marshal(data) @@ -121,6 +135,9 @@ func work(version string, ps *system.SystemPS, client *http.Client) { log.Println("E! failed to close gzip buffer:", err) return } + if config.Config.DebugMode { + log.Printf("D! heartbeat request: %s\n", string(bs)) + } req, err := http.NewRequest("POST", config.Config.Heartbeat.Url, &buf) if err != nil { @@ -130,7 +147,7 @@ func work(version string, ps *system.SystemPS, client *http.Client) { req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Encoding", "gzip") - req.Header.Set("User-Agent", "categraf/"+version) + req.Header.Set("User-Agent", "categraf/"+hostIP) for i := 0; i < len(config.Config.Heartbeat.Headers); i += 2 { req.Header.Add(config.Config.Heartbeat.Headers[i], config.Config.Heartbeat.Headers[i+1]) diff --git a/heartbeat/memory/memory.go b/heartbeat/memory/memory.go new file mode 100644 index 00000000..2500786c --- /dev/null +++ b/heartbeat/memory/memory.go @@ -0,0 +1,112 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package memory + +import ( + "fmt" + "log" + "strconv" + "strings" +) + +type Memory struct{} + +const name = "memory" + +func (self *Memory) Name() string { + return name +} + +func (self *Memory) Collect() (result interface{}, err error) { + mem, err := getMemoryInfo() + if total, ok := mem["total"]; ok { + var ( + newTotal = total + times = 1 + ) + t := strings.ToLower(total) + switch { + case strings.HasSuffix(t, "k"): + newTotal = strings.TrimSuffix(t, "k") + times = 1024 + + case strings.HasSuffix(t, "kb"): + newTotal = strings.TrimSuffix(t, "kb") + times = 1024 + + case strings.HasSuffix(t, "m"): + newTotal = strings.TrimSuffix(t, "m") + times = 1024 * 1024 + case strings.HasSuffix(t, "mb"): + newTotal = strings.TrimSuffix(t, "mb") + times = 1024 * 1024 + + case strings.HasSuffix(t, "g"): + newTotal = strings.TrimSuffix(t, "g") + times = 1024 * 1024 * 1024 + case strings.HasSuffix(t, "gb"): + newTotal = strings.TrimSuffix(t, "gb") + times = 1024 * 1024 * 1024 + + } + tv, e := convert(newTotal, times) + if e != nil { + log.Printf("W! parse memory total [%s||%s||%s] error: %s", total, t, newTotal, e) + err = e + } else { + mem["total"] = fmt.Sprintf("%d", int64(tv)) + } + } + + if swap, ok := mem["swap_total"]; ok { + var ( + newSwap = swap + times = 1 + ) + s := strings.ToLower(swap) + switch { + case strings.HasSuffix(s, "k"): + newSwap = strings.TrimSuffix(s, "k") + times = 1024 + + case strings.HasSuffix(s, "kb"): + newSwap = strings.TrimSuffix(s, "kb") + times = 1024 + + case strings.HasSuffix(s, "m"): + newSwap = strings.TrimSuffix(s, "m") + times = 1024 * 1024 + case strings.HasSuffix(s, "mb"): + newSwap = strings.TrimSuffix(s, "mb") + times = 1024 * 1024 + + case strings.HasSuffix(s, "g"): + newSwap = strings.TrimSuffix(s, "g") + times = 1024 * 1024 * 1024 + case strings.HasSuffix(s, "gb"): + newSwap = strings.TrimSuffix(s, "gb") + times = 1024 * 1024 * 1024 + + } + tv, e := convert(newSwap, times) + if e != nil { + log.Printf("W! parse memory swap [%s||%s||%s] error: %s", swap, s, newSwap, err) + err = e + } else { + mem["swap_total"] = fmt.Sprintf("%d", int64(tv)) + } + } + + return mem, err +} + +func convert(total string, times int) (float64, error) { + t, err := strconv.ParseFloat(total, 64) + if err != nil { + return 0, err + } + return t * float64(times), nil +} diff --git a/heartbeat/memory/memory_darwin.go b/heartbeat/memory/memory_darwin.go new file mode 100644 index 00000000..f1788a9a --- /dev/null +++ b/heartbeat/memory/memory_darwin.go @@ -0,0 +1,29 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package memory + +import ( + "os/exec" + "regexp" + "strings" +) + +func getMemoryInfo() (memoryInfo map[string]string, err error) { + memoryInfo = make(map[string]string) + + out, err := exec.Command("sysctl", "-n", "hw.memsize").Output() + if err == nil { + memoryInfo["total"] = strings.Trim(string(out), "\n") + } + + out, err = exec.Command("sysctl", "-n", "vm.swapusage").Output() + if err == nil { + swap := regexp.MustCompile("total = ").Split(string(out), 2)[1] + memoryInfo["swap_total"] = strings.Split(swap, " ")[0] + } + + return +} diff --git a/heartbeat/memory/memory_linux.go b/heartbeat/memory/memory_linux.go new file mode 100644 index 00000000..57705b8c --- /dev/null +++ b/heartbeat/memory/memory_linux.go @@ -0,0 +1,52 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package memory + +import ( + "bufio" + "fmt" + "os" + "regexp" +) + +var memMap = map[string]string{ + "MemTotal": "total", + "SwapTotal": "swap_total", +} + +func getMemoryInfo() (memoryInfo map[string]string, err error) { + file, err := os.Open("/proc/meminfo") + + if err != nil { + return + } + + var lines []string + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + if scanner.Err() != nil { + err = scanner.Err() + return + } + + memoryInfo = make(map[string]string) + + for _, line := range lines { + pair := regexp.MustCompile(": +").Split(line, 2) + values := regexp.MustCompile(" +").Split(pair[1], 2) + + key, ok := memMap[pair[0]] + if ok { + memoryInfo[key] = fmt.Sprintf("%s%s", values[0], values[1]) + } + } + + return +} diff --git a/heartbeat/memory/memory_windows.go b/heartbeat/memory/memory_windows.go new file mode 100644 index 00000000..603dffb8 --- /dev/null +++ b/heartbeat/memory/memory_windows.go @@ -0,0 +1,42 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package memory + +import ( + "strconv" + "syscall" + "unsafe" +) + +type MEMORYSTATUSEX struct { + dwLength uint32 // size of this structure + dwMemoryLoad uint32 // number 0-100 estimating %age of memory in use + ulTotalPhys uint64 // amount of physical memory + ulAvailPhys uint64 // amount of physical memory that can be used w/o flush to disk + ulTotalPageFile uint64 // current commit limit for system or process + ulAvailPageFile uint64 // amount of memory current process can commit + ulTotalVirtual uint64 // size of user-mode portion of VA space + ulAvailVirtual uint64 // amount of unreserved/uncommitted memory in ulTotalVirtual + ulAvailExtendedVirtual uint64 // reserved (always zero) +} + +func getMemoryInfo() (memoryInfo map[string]string, err error) { + memoryInfo = make(map[string]string) + + var mod = syscall.NewLazyDLL("kernel32.dll") + var getMem = mod.NewProc("GlobalMemoryStatusEx") + + var mem_struct MEMORYSTATUSEX + + mem_struct.dwLength = uint32(unsafe.Sizeof(mem_struct)) + + status, _, err := getMem.Call(uintptr(unsafe.Pointer(&mem_struct))) + if status != 0 { + memoryInfo["total"] = strconv.FormatUint(mem_struct.ulTotalPhys, 10) + err = nil + } + return +} diff --git a/heartbeat/meta.go b/heartbeat/meta.go new file mode 100644 index 00000000..f9c88ff3 --- /dev/null +++ b/heartbeat/meta.go @@ -0,0 +1,85 @@ +package heartbeat + +import ( + "log" + "time" + + "flashcat.cloud/categraf/heartbeat/cpu" + "flashcat.cloud/categraf/heartbeat/filesystem" + "flashcat.cloud/categraf/heartbeat/memory" + "flashcat.cloud/categraf/heartbeat/network" + "flashcat.cloud/categraf/heartbeat/platform" +) + +type ( + SystemInfo struct { + CPU interface{} `json:"cpu"` + Memory interface{} `json:"memory"` + Network interface{} `json:"network"` + Platform interface{} `json:"platform"` + Filesystem interface{} `json:"filesystem"` + } +) + +var meta *SystemInfo + +func collectSystemInfo() (*SystemInfo, error) { + if meta != nil { + return meta, nil + } + timer := time.NewTimer(0 * time.Second) + defer timer.Stop() + + go func() { + for { + select { + case <-timer.C: + info, err := collect() + if err != nil { + log.Println("W!", "collectSystemInfo error:", err) + time.Sleep(1 * time.Second) + continue + } + meta = info + timer.Reset(1 * time.Minute) + } + } + }() + + info, err := collect() + meta = info + return meta, err +} + +func collect() (*SystemInfo, error) { + + cpuInfo, err := new(cpu.Cpu).Collect() + if err != nil { + return nil, err + } + + memInfo, err := new(memory.Memory).Collect() + if err != nil { + return nil, err + } + fs, err := new(filesystem.FileSystem).Collect() + if err != nil { + return nil, err + } + net, err := new(network.Network).Collect() + if err != nil { + return nil, err + } + pl, err := new(platform.Platform).Collect() + if err != nil { + return nil, err + } + + return &SystemInfo{ + CPU: cpuInfo, + Memory: memInfo, + Filesystem: fs, + Network: net, + Platform: pl, + }, nil +} diff --git a/heartbeat/network/ipconfig_test_sample.txt b/heartbeat/network/ipconfig_test_sample.txt new file mode 100644 index 00000000..0e03a114 Binary files /dev/null and b/heartbeat/network/ipconfig_test_sample.txt differ diff --git a/heartbeat/network/network.go b/heartbeat/network/network.go new file mode 100644 index 00000000..2399d058 --- /dev/null +++ b/heartbeat/network/network.go @@ -0,0 +1,36 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +//go:build linux || darwin +// +build linux darwin + +package network + +func getNetworkInfo() (networkInfo map[string]interface{}, err error) { + networkInfo = make(map[string]interface{}) + + macaddress, err := macAddress() + if err != nil { + return networkInfo, err + } + networkInfo["macaddress"] = macaddress + + ipAddress, err := externalIpAddress() + if err != nil { + return networkInfo, err + } + networkInfo["ipaddress"] = ipAddress + + ipAddressV6, err := externalIpv6Address() + if err != nil { + return networkInfo, err + } + // We append an IPv6 address to the payload only if IPv6 is enabled + if ipAddressV6 != "" { + networkInfo["ipaddressv6"] = ipAddressV6 + } + + return +} diff --git a/heartbeat/network/network_common.go b/heartbeat/network/network_common.go new file mode 100644 index 00000000..d9908057 --- /dev/null +++ b/heartbeat/network/network_common.go @@ -0,0 +1,196 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package network + +import ( + "errors" + "net" +) + +type Network struct{} + +const name = "network" + +func (self *Network) Name() string { + return name +} + +func (self *Network) Collect() (result interface{}, err error) { + result, err = getNetworkInfo() + if err != nil { + return + } + + interfaces, err := getMultiNetworkInfo() + if err == nil && len(interfaces) > 0 { + interfaceMap, ok := result.(map[string]interface{}) + if !ok { + return + } + interfaceMap["interfaces"] = interfaces + } + return +} + +func getMultiNetworkInfo() (multiNetworkInfo []map[string]interface{}, err error) { + ifaces, err := net.Interfaces() + + if err != nil { + return multiNetworkInfo, err + } + for _, iface := range ifaces { + _iface := make(map[string]interface{}) + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + // interface down or loopback interface + continue + } + addrs, err := iface.Addrs() + if err != nil { + // skip this interface but try the next + continue + } + for _, addr := range addrs { + ip, network, _ := net.ParseCIDR(addr.String()) + if ip == nil || ip.IsLoopback() { + continue + } + if ip.To4() == nil { + _iface["ipv6"] = ip.String() + _iface["ipv6-network"] = network.String() + } else { + _iface["ipv4"] = ip.String() + _iface["ipv4-network"] = network.String() + } + if len(iface.HardwareAddr.String()) > 0 { + _iface["macaddress"] = iface.HardwareAddr.String() + } + } + if len(_iface) > 0 { + _iface["name"] = iface.Name + multiNetworkInfo = append(multiNetworkInfo, _iface) + } + } + return multiNetworkInfo, err +} + +type Ipv6Address struct{} + +func externalIpv6Address() (string, error) { + ifaces, err := net.Interfaces() + + if err != nil { + return "", err + } + + for _, iface := range ifaces { + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + // interface down or loopback interface + continue + } + addrs, err := iface.Addrs() + if err != nil { + return "", err + } + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + if ip == nil || ip.IsLoopback() { + continue + } + if ip.To4() != nil { + // ipv4 address + continue + } + return ip.String(), nil + } + } + + // We don't return an error if no IPv6 interface has been found. Indeed, + // some orgs just don't have IPv6 enabled. If there's a network error, it + // will pop out when getting the Mac address and/or the IPv4 address + // (before this function's call; see network.go -> getNetworkInfo()) + return "", nil +} + +type IpAddress struct{} + +func externalIpAddress() (string, error) { + ifaces, err := net.Interfaces() + + if err != nil { + return "", err + } + + for _, iface := range ifaces { + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + // interface down or loopback interface + continue + } + addrs, err := iface.Addrs() + if err != nil { + return "", err + } + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + if ip == nil || ip.IsLoopback() { + continue + } + ip = ip.To4() + if ip == nil { + // not an ipv4 address + continue + } + return ip.String(), nil + } + } + return "", errors.New("not connected to the network") +} + +type MacAddress struct{} + +func macAddress() (string, error) { + ifaces, err := net.Interfaces() + + if err != nil { + return "", err + } + + for _, iface := range ifaces { + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + // interface down or loopback interface + continue + } + addrs, err := iface.Addrs() + if err != nil { + return "", err + } + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + if ip == nil || ip.IsLoopback() || ip.To4() == nil { + continue + } + return iface.HardwareAddr.String(), nil + } + } + return "", errors.New("not connected to the network") +} diff --git a/heartbeat/network/network_windows.go b/heartbeat/network/network_windows.go new file mode 100644 index 00000000..89d78af9 --- /dev/null +++ b/heartbeat/network/network_windows.go @@ -0,0 +1,98 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package network + +import ( + "errors" + "os/exec" + "strings" +) + +func getNetworkInfo() (networkInfo map[string]interface{}, err error) { + networkInfo = make(map[string]interface{}) + + out, err := exec.Command("ipconfig", "-all").CombinedOutput() + if err != nil { + return + } + + networkInfo, err = parseIpConfig(string(out)) + return +} + +func parseIpConfig(out string) (networkInfo map[string]interface{}, err error) { + // The hardest part is that we want the 3 addresses to come from the same block + // or else, it wouldn't really make sense. Also we assume that only one + // interface is seriously enabled and has IPv4 at least + networkInfo = make(map[string]interface{}) + var ip, mac, ipv6 string + + lines := strings.Split(string(out), "\n") + gottablock := false + for _, line := range lines { + // The line below is here in case we have to convert some Unicode to ASCII + // It shouldn't do anything on Windows but when running the tests (for + // Windows) on a Unix based-system, it's essential. + line = strings.Replace(line, "\x00", "", -1) + + if strings.Contains(line, "IPv4") { + ip = line + gottablock = true + } else if (strings.Contains(line, "Physical Address") || strings.Contains(line, "物理地址")) && mac == "" { + mac = line + } else if strings.Contains(line, "IPv6") && ipv6 == "" { + ipv6 = line + } + // Whenever we reach the end of a block + if isEmptyString(line) { + if gottablock && mac != "" { + break + } else { // Or something's wrong... let's try again with the next block + gottablock = false + ip, mac, ipv6 = "", "", "" + } + } + } + + elt := strings.Split(ip, ": ") + if len(elt) >= 2 { + if strings.Contains(elt[1], "(Preferred)") { + networkInfo["ipaddress"] = strings.Trim(strings.Trim(elt[1], "\r"), "(Preferred) ") + } + if strings.Contains(elt[1], "(首选)") { + networkInfo["ipaddress"] = strings.Trim(strings.Trim(elt[1], "\r"), "(首选) ") + } + } else { + return networkInfo, errors.New("not connected to the network") + } + + // We're sure to have a mac address at this point, no paranoia check needed + elt = strings.Split(mac, ": ") + if strings.Contains(elt[1], "(Preferred)") { + networkInfo["macaddress"] = strings.Trim(strings.Trim(elt[1], "\r"), "(Preferred) ") + } + if strings.Contains(elt[1], "(首选)") { + networkInfo["macaddress"] = strings.Replace(strings.Trim(elt[1], "\r"), "(首选) ", "", -1) + } + + // But some interfaces still don't like IPv6 (or have it turned off) + elt = strings.Split(ipv6, ": ") + if len(elt) >= 2 { + if strings.Contains(elt[1], "(Preferred)") { + networkInfo["ipaddressv6"] = strings.Replace(strings.Trim(elt[1], "\r"), "(Preferred) ", "", -1) + } + if strings.Contains(elt[1], "(首选)") { + networkInfo["ipaddressv6"] = strings.Replace(strings.Trim(elt[1], "\r"), "(首选) ", "", -1) + } + } else { + networkInfo["ipaddressv6"] = "" + } + return +} + +func isEmptyString(val string) bool { + return val == "\r" || val == "" +} diff --git a/heartbeat/platform/platform.go b/heartbeat/platform/platform.go new file mode 100644 index 00000000..17ac59b5 --- /dev/null +++ b/heartbeat/platform/platform.go @@ -0,0 +1,37 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +//go:build linux || darwin +// +build linux darwin + +package platform + +import ( + "fmt" + "os/exec" + "regexp" + "strings" +) + +// GetArchInfo() returns basic host architecture information +func GetArchInfo() (archInfo map[string]interface{}, err error) { + archInfo = make(map[string]interface{}) + + out, err := exec.Command("uname", unameOptions...).Output() + if err != nil { + return nil, err + } + line := fmt.Sprintf("%s", out) + values := regexp.MustCompile(" +").Split(line, 7) + updateArchInfo(archInfo, values) + + out, err = exec.Command("uname", "-v").Output() + if err != nil { + return nil, err + } + archInfo["kernel_version"] = strings.Trim(string(out), "\n") + + return +} diff --git a/heartbeat/platform/platform_android.go b/heartbeat/platform/platform_android.go new file mode 100644 index 00000000..fbab244e --- /dev/null +++ b/heartbeat/platform/platform_android.go @@ -0,0 +1,27 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +//go:build android +// +build android + +package platform + +type Platform struct{} + +const name = "platform" + +func (self *Platform) Name() string { + return name +} + +func (self *Platform) Collect() (result interface{}, err error) { + result, err = getPlatformInfo() + return +} + +func getPlatformInfo() (platformInfo map[string]interface{}, err error) { + + return +} diff --git a/heartbeat/platform/platform_common.go b/heartbeat/platform/platform_common.go new file mode 100644 index 00000000..e988bf84 --- /dev/null +++ b/heartbeat/platform/platform_common.go @@ -0,0 +1,71 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +//go:build !android +// +build !android + +package platform + +import ( + "fmt" + "os/exec" + "regexp" + "runtime" + "strings" +) + +type Platform struct{} + +const name = "platform" + +func (self *Platform) Name() string { + return name +} + +func (self *Platform) Collect() (result interface{}, err error) { + result, err = getPlatformInfo() + return +} + +func getPlatformInfo() (platformInfo map[string]interface{}, err error) { + + // collect each portion, and allow the parts that succeed (even if some + // parts fail.) For this check, it does have the (small) liability + // that if both the ArchInfo() and the PythonVersion() fail, the error + // from the ArchInfo() will be lost + + // for this, no error check. The successful results will be added + // to the return value, and the error stored. + platformInfo, err = GetArchInfo() + if platformInfo == nil { + platformInfo = make(map[string]interface{}) + } + + platformInfo["goV"] = strings.Replace(runtime.Version(), "go", "", -1) + // If this errors, swallow the error. + // It will usually mean that Python is not on the PATH + // and we don't care about that. + pythonV, e := getPythonVersion() + + // if there was no failure, add the python variables to the platformInfo + if e == nil { + platformInfo["pythonV"] = pythonV + } + + platformInfo["GOOS"] = runtime.GOOS + platformInfo["GOARCH"] = runtime.GOARCH + + return +} + +func getPythonVersion() (string, error) { + out, err := exec.Command("python", "-V").CombinedOutput() + if err != nil { + return "", err + } + version := fmt.Sprintf("%s", out) + values := regexp.MustCompile("Python (.*)\n").FindStringSubmatch(version) + return strings.Trim(values[1], "\r"), nil +} diff --git a/heartbeat/platform/platform_darwin.go b/heartbeat/platform/platform_darwin.go new file mode 100644 index 00000000..a5cbe715 --- /dev/null +++ b/heartbeat/platform/platform_darwin.go @@ -0,0 +1,45 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package platform + +import ( + "log" + "strings" + + "golang.org/x/sys/unix" +) + +var unameOptions = []string{"-s", "-n", "-r", "-m", "-p"} + +// processIsTranslated detects if the process using gohai is running under the Rosetta 2 translator +func processIsTranslated() (bool, error) { + // https://developer.apple.com/documentation/apple_silicon/about_the_rosetta_translation_environment#3616845 + ret, err := unix.SysctlUint32("sysctl.proc_translated") + + if err == nil { + return ret == 1, nil + } else if err.(unix.Errno) == unix.ENOENT { + return false, nil + } + return false, err +} + +func updateArchInfo(archInfo map[string]interface{}, values []string) { + archInfo["kernel_name"] = values[0] + archInfo["hostname"] = values[1] + archInfo["kernel_release"] = values[2] + archInfo["machine"] = values[3] + archInfo["processor"] = strings.Trim(values[4], "\n") + archInfo["os"] = values[0] + + if isTranslated, err := processIsTranslated(); err == nil && isTranslated { + log.Println("Running under Rosetta translator; overriding architecture values") + archInfo["processor"] = "arm" + archInfo["machine"] = "arm64" + } else if err != nil { + log.Printf("Error when detecting Rosetta translator: %s", err) + } +} diff --git a/heartbeat/platform/platform_linux.go b/heartbeat/platform/platform_linux.go new file mode 100644 index 00000000..245a3f05 --- /dev/null +++ b/heartbeat/platform/platform_linux.go @@ -0,0 +1,20 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package platform + +import "strings" + +var unameOptions = []string{"-s", "-n", "-r", "-m", "-p", "-i", "-o"} + +func updateArchInfo(archInfo map[string]interface{}, values []string) { + archInfo["kernel_name"] = values[0] + archInfo["hostname"] = values[1] + archInfo["kernel_release"] = values[2] + archInfo["machine"] = values[3] + archInfo["processor"] = values[4] + archInfo["hardware_platform"] = values[5] + archInfo["os"] = strings.Trim(values[6], "\n") +} diff --git a/heartbeat/platform/platform_windows.go b/heartbeat/platform/platform_windows.go new file mode 100644 index 00000000..a0be0f5a --- /dev/null +++ b/heartbeat/platform/platform_windows.go @@ -0,0 +1,173 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package platform + +import ( + "fmt" + "os" + "runtime" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" +) + +var ( + modNetapi32 = windows.NewLazyDLL("Netapi32.dll") + procNetWkstaGetInfo = modNetapi32.NewProc("NetWkstaGetInfo") + procNetServerGetInfo = modNetapi32.NewProc("NetServerGetInfo") + procNetApiBufferFree = modNetapi32.NewProc("NetApiBufferFree") +) + +const ( + SV_TYPE_WORKSTATION = uint32(0x00000001) + SV_TYPE_SERVER = uint32(0x00000002) + SV_TYPE_SQLSERVER = uint32(0x00000004) + SV_TYPE_DOMAIN_CTRL = uint32(0x00000008) + SV_TYPE_DOMAIN_BAKCTRL = uint32(0x00000010) + SV_TYPE_TIME_SOURCE = uint32(0x00000020) + SV_TYPE_AFP = uint32(0x00000040) + SV_TYPE_NOVELL = uint32(0x00000080) + SV_TYPE_DOMAIN_MEMBER = uint32(0x00000100) + SV_TYPE_PRINTQ_SERVER = uint32(0x00000200) + SV_TYPE_DIALIN_SERVER = uint32(0x00000400) + SV_TYPE_XENIX_SERVER = uint32(0x00000800) + SV_TYPE_SERVER_UNIX = SV_TYPE_XENIX_SERVER + SV_TYPE_NT = uint32(0x00001000) + SV_TYPE_WFW = uint32(0x00002000) + SV_TYPE_SERVER_MFPN = uint32(0x00004000) + SV_TYPE_SERVER_NT = uint32(0x00008000) + SV_TYPE_POTENTIAL_BROWSER = uint32(0x00010000) + SV_TYPE_BACKUP_BROWSER = uint32(0x00020000) + SV_TYPE_MASTER_BROWSER = uint32(0x00040000) + SV_TYPE_DOMAIN_MASTER = uint32(0x00080000) + SV_TYPE_SERVER_OSF = uint32(0x00100000) + SV_TYPE_SERVER_VMS = uint32(0x00200000) + SV_TYPE_WINDOWS = uint32(0x00400000) /* Windows95 and above */ + SV_TYPE_DFS = uint32(0x00800000) /* Root of a DFS tree */ + SV_TYPE_CLUSTER_NT = uint32(0x01000000) /* NT Cluster */ + SV_TYPE_TERMINALSERVER = uint32(0x02000000) /* Terminal Server(Hydra) */ + SV_TYPE_CLUSTER_VS_NT = uint32(0x04000000) /* NT Cluster Virtual Server Name */ + SV_TYPE_DCE = uint32(0x10000000) /* IBM DSS (Directory and Security Services) or equivalent */ + SV_TYPE_ALTERNATE_XPORT = uint32(0x20000000) /* return list for alternate transport */ + SV_TYPE_LOCAL_LIST_ONLY = uint32(0x40000000) /* Return local list only */ + SV_TYPE_DOMAIN_ENUM = uint32(0x80000000) + SV_TYPE_ALL = uint32(0xFFFFFFFF) /* handy for NetServerEnum2 */ +) +const registryHive = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion" +const productNameKey = "ProductName" +const buildNumberKey = "CurrentBuildNumber" +const majorKey = "CurrentMajorVersionNumber" +const minorKey = "CurrentMinorVersionNumber" + +func GetVersion() (maj uint64, min uint64, err error) { + + var outdata *byte + if err = modNetapi32.Load(); err != nil { + return + } + if err = procNetWkstaGetInfo.Find(); err != nil { + return + } + status, _, err := procNetWkstaGetInfo.Call(uintptr(0), uintptr(100), uintptr(unsafe.Pointer(&outdata))) + if status != uintptr(0) { + return 0, 0, err + } + defer procNetApiBufferFree.Call(uintptr(unsafe.Pointer(outdata))) + return platGetVersion(outdata) + +} + +func netServerGetInfo() (si SERVER_INFO_101, err error) { + var outdata *byte + // do additional work so that we don't panic() when the library's + // not there (like in a container) + if err = modNetapi32.Load(); err != nil { + return + } + if err = procNetServerGetInfo.Find(); err != nil { + return + } + status, _, err := procNetServerGetInfo.Call(uintptr(0), uintptr(101), uintptr(unsafe.Pointer(&outdata))) + if status != uintptr(0) { + return + } + defer procNetApiBufferFree.Call(uintptr(unsafe.Pointer(outdata))) + return platGetServerInfo(outdata), nil +} + +// GetArchInfo() returns basic host architecture information +func GetArchInfo() (systemInfo map[string]interface{}, err error) { + systemInfo = make(map[string]interface{}) + + systemInfo["hostname"], _ = os.Hostname() + + if runtime.GOARCH == "amd64" { + systemInfo["machine"] = "x86_64" + } else { + systemInfo["machine"] = runtime.GOARCH + } + + k, err := registry.OpenKey(registry.LOCAL_MACHINE, + registryHive, + registry.QUERY_VALUE) + defer k.Close() + + systemInfo["os"], _, _ = k.GetStringValue(productNameKey) + + var maj, _, _ = k.GetIntegerValue(majorKey) + var min, _, _ = k.GetIntegerValue(minorKey) + var bld, _, _ = k.GetStringValue(buildNumberKey) + if maj == 0 { + maj, min, err = GetVersion() + if 0 != syscall.Errno(0) { + return + } + } + verstring := fmt.Sprintf("%d.%d.%s", maj, min, bld) + systemInfo["kernel_release"] = verstring + + systemInfo["kernel_name"] = "Windows" + + // do additional work so that we don't panic() when the library's + // not there (like in a container) + family := "Unknown" + si, sierr := netServerGetInfo() + if sierr == nil { + if (si.sv101_type&SV_TYPE_WORKSTATION) == SV_TYPE_WORKSTATION || + (si.sv101_type&SV_TYPE_SERVER) == SV_TYPE_SERVER { + if (si.sv101_type & SV_TYPE_WORKSTATION) == SV_TYPE_WORKSTATION { + family = "Workstation" + } else if (si.sv101_type & SV_TYPE_SERVER) == SV_TYPE_SERVER { + family = "Server" + } + if (si.sv101_type & SV_TYPE_DOMAIN_MEMBER) == SV_TYPE_DOMAIN_MEMBER { + family = "Domain Joined " + family + } else { + family = "Standalone " + family + } + } else if (si.sv101_type & SV_TYPE_DOMAIN_CTRL) == SV_TYPE_DOMAIN_CTRL { + family = "Domain Controller" + } else if (si.sv101_type & SV_TYPE_DOMAIN_BAKCTRL) == SV_TYPE_DOMAIN_BAKCTRL { + family = "Backup Domain Controller" + } + } + systemInfo["family"] = family + + return +} + +func convert_windows_string(winput []uint16) string { + var retstring string + for i := 0; i < len(winput); i++ { + if winput[i] == 0 { + break + } + retstring += string(rune(winput[i])) + } + return retstring +} diff --git a/heartbeat/platform/platform_windows_386.go b/heartbeat/platform/platform_windows_386.go new file mode 100644 index 00000000..08a124c4 --- /dev/null +++ b/heartbeat/platform/platform_windows_386.go @@ -0,0 +1,75 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package platform + +import ( + "encoding/binary" + "unsafe" +) + +type WKSTA_INFO_100 struct { + wki100_platform_id uint32 + wki100_computername string + wki100_langroup string + wki100_ver_major uint32 + wki100_ver_minor uint32 +} + +type SERVER_INFO_101 struct { + sv101_platform_id uint32 + sv101_name string + sv101_version_major uint32 + sv101_version_minor uint32 + sv101_type uint32 + sv101_comment string +} + +func byteArrayToWksaInfo(data []byte) (info WKSTA_INFO_100) { + info.wki100_platform_id = binary.LittleEndian.Uint32(data) + + // if necessary, convert the pointer to a c-string into a GO string. + // Not using at this time. However, leaving as a placeholder, to + // show why we're skipping 4 bytes of the buffer here... + + // addr := (*byte)(unsafe.Pointer(uintptr(binary.LittleEndian.Uint64(data[4:])))) + // info.wki100_computername = addr + + // ... and again here for the lan group name. + // stringptr = (*[]byte)(unsafe.Pointer(uintptr(binary.LittleEndian.Uint64(data[8:])))) + // info.wki100_langroup = convert_windows_string(stringptr) + + info.wki100_ver_major = binary.LittleEndian.Uint32(data[12:]) + info.wki100_ver_minor = binary.LittleEndian.Uint32(data[16:]) + return +} +func platGetVersion(outdata *byte) (maj uint64, min uint64, err error) { + var info WKSTA_INFO_100 + var dataptr []byte + dataptr = (*[20]byte)(unsafe.Pointer(outdata))[:] + + info = byteArrayToWksaInfo(dataptr) + maj = uint64(info.wki100_ver_major) + min = uint64(info.wki100_ver_minor) + return +} + +func platGetServerInfo(data *byte) (si101 SERVER_INFO_101) { + var outdata []byte + outdata = (*[24]byte)(unsafe.Pointer(data))[:] + si101.sv101_platform_id = binary.LittleEndian.Uint32(outdata) + + // stringptr := (*[]uint16)(unsafe.Pointer(uintptr(binary.LittleEndian.Uint64(outdata[4:])))) + // si101.sv101_name = convert_windows_string(*stringptr) + + si101.sv101_version_major = binary.LittleEndian.Uint32(outdata[8:]) + si101.sv101_version_minor = binary.LittleEndian.Uint32(outdata[12:]) + si101.sv101_type = binary.LittleEndian.Uint32(outdata[16:]) + + // stringptr = (*[]uint16)(unsafe.Pointer(uintptr(binary.LittleEndian.Uint32(outdata[20:])))) + // si101.sv101_comment = convert_windows_string(*stringptr) + return + +} diff --git a/heartbeat/platform/platform_windows_amd64.go b/heartbeat/platform/platform_windows_amd64.go new file mode 100644 index 00000000..341eb5a5 --- /dev/null +++ b/heartbeat/platform/platform_windows_amd64.go @@ -0,0 +1,79 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package platform + +import ( + "encoding/binary" + "unsafe" +) + +type WKSTA_INFO_100 struct { + wki100_platform_id uint32 + wki100_computername string + wki100_langroup string + wki100_ver_major uint32 + wki100_ver_minor uint32 +} + +type SERVER_INFO_101 struct { + sv101_platform_id uint32 + sv101_name string + sv101_version_major uint32 + sv101_version_minor uint32 + sv101_type uint32 + sv101_comment string +} + +func byteArrayToWksaInfo(data []byte) (info WKSTA_INFO_100) { + info.wki100_platform_id = binary.LittleEndian.Uint32(data) + + // the specified return type of wki100_platform_id is uint32. However, + // due to 64 bit packing, we actually have to skip 8 bytes. + + // if necessary, convert the pointer to a c-string into a GO string. + // Not using at this time. However, leaving as a placeholder, to + // show why we're skipping 8 bytes of the buffer here... + + // addr := (*byte)(unsafe.Pointer(uintptr(binary.LittleEndian.Uint64(data[8:])))) + // info.wki100_computername = addr + + // ... and again here for the lan group name. + // stringptr = (*[]byte)(unsafe.Pointer(uintptr(binary.LittleEndian.Uint64(data[16:])))) + // info.wki100_langroup = convert_windows_string(stringptr) + + info.wki100_ver_major = binary.LittleEndian.Uint32(data[24:]) + info.wki100_ver_minor = binary.LittleEndian.Uint32(data[28:]) + return +} +func platGetVersion(outdata *byte) (maj uint64, min uint64, err error) { + var info WKSTA_INFO_100 + var dataptr []byte + dataptr = (*[32]byte)(unsafe.Pointer(outdata))[:] + + info = byteArrayToWksaInfo(dataptr) + maj = uint64(info.wki100_ver_major) + min = uint64(info.wki100_ver_minor) + return +} + +func platGetServerInfo(data *byte) (si101 SERVER_INFO_101) { + var outdata []byte + outdata = (*[40]byte)(unsafe.Pointer(data))[:] + si101.sv101_platform_id = binary.LittleEndian.Uint32(outdata) + + // due to 64 bit packing, skip 8 bytes to get to the name string + // stringptr := *(*[]uint16)(unsafe.Pointer(uintptr(binary.LittleEndian.Uint64(outdata[8:])))) + // si101.sv101_name = convert_windows_string(stringptr) + + si101.sv101_version_major = binary.LittleEndian.Uint32(outdata[16:]) + si101.sv101_version_minor = binary.LittleEndian.Uint32(outdata[20:]) + si101.sv101_type = binary.LittleEndian.Uint32(outdata[24:]) + + // again skip 4 more for byte packing, so start at 32 + // stringptr = (*[]uint16)(unsafe.Pointer(uintptr(binary.LittleEndian.Uint32(outdata[32:])))) + // si101.sv101_comment = convert_windows_string(*stringptr) + return +} diff --git a/heartbeat/platform/platform_windows_arm64.go b/heartbeat/platform/platform_windows_arm64.go new file mode 100644 index 00000000..0a52422d --- /dev/null +++ b/heartbeat/platform/platform_windows_arm64.go @@ -0,0 +1,31 @@ +// This file is licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright © 2015 Kentaro Kuribayashi +// Copyright 2014-present Datadog, Inc. + +package platform + +type WKSTA_INFO_100 struct { + wki100_platform_id uint32 + wki100_computername string + wki100_langroup string + wki100_ver_major uint32 + wki100_ver_minor uint32 +} + +type SERVER_INFO_101 struct { + sv101_platform_id uint32 + sv101_name string + sv101_version_major uint32 + sv101_version_minor uint32 + sv101_type uint32 + sv101_comment string +} + +func platGetVersion(outdata *byte) (maj uint64, min uint64, err error) { + return 0, 0, nil +} + +func platGetServerInfo(data *byte) (si101 SERVER_INFO_101) { + return SERVER_INFO_101{} +}