Forum Discussion

Joe_Williams's avatar
Joe_Williams
Icon for Professor rankProfessor
2 months ago

Silent Log Source Detection

We are working on migrating some items to LM Logs and one thing that came up was the lack of Silent Log Source detection. Log Usage is a PushModule and only updates when data is pushed to it. So unlike a traditional DataSource, you cannot alert on No Data or 0 events for X period of time. With that in mind, we wrote our own, which I am sharing here instead of in the LM Exchange as it will require a bit of changing on your own.

So, this could be 100% portable, but I purposefully didn't do that to save on API calls. That this DataSource will do, is make two api calls per device, back to the LogicMonitor portal. The first is to retrieve the instances of LogUsage, which is defined in line 22. That is where you would need to change the ID to match what is in your portal. Then after it retrieves the instance of LogUsage, it queries data for said instance. That returns a JSON structure like this.

{
	"dataPoints": [
		"size_in_bytes",
		"event_received"
	],
	"dataSourceName": "LogUsage",
	"nextPageParams": "start=1739704983&end=1741035719",
	"time": [
		1741110300000
	],
	"values": [
		[
			25942,
			16
		]
}

There will generally be more items in time and in values. We grab the first element, so [0] of the time array as that is the latest timestamp. Then we calculate the difference between now and that timestamp in hours. Then we return the difference in hours, or a NaN.

/*******************************************************************************
 * © 2007-2024 - LogicMonitor, Inc. All rights reserved.
 ******************************************************************************/

import groovy.json.JsonSlurper
import com.santaba.agent.util.Settings
import com.santaba.agent.live.LiveHostSet
import org.apache.commons.codec.binary.Hex
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec


String apiId   = hostProps.get("lmaccess.id")  ?: hostProps.get("logicmonitor.access.id")
String apiKey  = hostProps.get("lmaccess.key") ?: hostProps.get("logicmonitor.access.key")
def portalName = hostProps.get("lmaccount")    ?: Settings.getSetting(Settings.AGENT_COMPANY)
String deviceid = hostProps.get("system.deviceId")

Map proxyInfo  = getProxyInfo()

def fields = 'id,dataSourceId,deviceDataSourceId,name,lastCollectedTime,lastUpdatedTime,deviceDataSourceId'
def apipath = "/device/devices/" + deviceid + "/instances"
def apifilter = 'dataSourceId:43806042'
def deviceinstances = apiGetMany(portalName, apiId, apiKey, apipath, proxyInfo, ['size':1000, 'fields': fields, 'filter': apifilter])
instanceid = deviceinstances[0]['id']
devicedatasourceid = deviceinstances[0]['deviceDataSourceId']

def instancepath = "/device/devices/" + deviceid + "/devicedatasources/" + devicedatasourceid + "/instances/" + instanceid + '/data'
def instancedata = apiGet(portalName, apiId, apiKey, instancepath, proxyInfo, ['period':720])

def now = System.currentTimeMillis()

if (instancedata['time'][0] != null && instancedata['time'][0] > 0) {
	def diffHours = ((instancedata['time'][0] - now) / (1000 * 60 * 60)).toDouble().round(2)
	println "hours_since_log=${diffHours}"
}
else {
	def diffHours = "NaN"
	println "hours_since_log=${diffHours}"
}

// If script gets to this point, collector should consider this device alive
keepAlive(hostProps)

return 0


/* Paginated GET method. Returns a list of objects. */
List apiGetMany(portalName, apiId, apiKey, endPoint, proxyInfo, Map args=[:]) {

    def pageSize = args.get('size', 1000) // Default the page size to 1000 if not specified.
    List items = []

    args['size'] = pageSize

    def pageCount = 0
    while (true) {
        pageCount += 1

        // Updated the args
        args['size'] = pageSize
        args['offset'] = items.size()

        def response = apiGet(portalName, apiId, apiKey, endPoint, proxyInfo, args)
        if (response.get("errmsg", "OK") != "OK") {
            throw new Exception("Santaba returned errormsg: ${response?.errmsg}")
        }
        items.addAll(response.items)

        // If we recieved less than we asked for it means we are done
        if (response.items.size() < pageSize) break
    }
    return items
}


/* Simple GET, returns a parsed json payload. No processing. */
def apiGet(portalName, apiId, apiKey, endPoint, proxyInfo, Map args=[:]) {
    def request = rawGet(portalName, apiId, apiKey, endPoint, proxyInfo, args)
    if (request.getResponseCode() == 200) {
        def payload = new JsonSlurper().parseText(request.content.text)
        return payload
    }
    else {
        throw new Exception("Server return HTTP code ${request.getResponseCode()}")
    }
}


/* Raw GET method. */
def rawGet(portalName, apiId, apiKey, endPoint, proxyInfo, Map args=[:]) {
    def auth = generateAuth(apiId, apiKey, endPoint)
    def headers = ["Authorization": auth, "Content-Type": "application/json", "X-Version":"3", "External-User":"true"]
    def url = "https://${portalName}.logicmonitor.com/santaba/rest${endPoint}"

    if (args) {
        def encodedArgs = []
        args.each{ k,v ->
            encodedArgs << "${k}=${java.net.URLEncoder.encode(v.toString(), "UTF-8")}"
        }
        url += "?${encodedArgs.join('&')}"
    }

    def request
    if (proxyInfo.enabled) {
        request = url.toURL().openConnection(proxyInfo.proxy)
    }
    else {
        request = url.toURL().openConnection()
    }
    request.setRequestMethod("GET")
    request.setDoOutput(true)
    headers.each{ k,v ->
        request.addRequestProperty(k, v)
    }

    return request
}


/* Generate auth for API calls. */
static String generateAuth(id, key, path) {
    Long epoch_time = System.currentTimeMillis()
    Mac hmac = Mac.getInstance("HmacSHA256")
    hmac.init(new SecretKeySpec(key.getBytes(), "HmacSHA256"))
    def signature = Hex.encodeHexString(hmac.doFinal("GET${epoch_time}${path}".getBytes())).bytes.encodeBase64()

    return "LMv1 ${id}:${signature}:${epoch_time}"
}


/* Helper method to remind the collector this device is not dead */
def keepAlive(hostProps) {
    // Update the liveHost set so tell the collector we are happy.
    hostId = hostProps.get("system.deviceId").toInteger()
    def liveHostSet =  LiveHostSet.getInstance()
    liveHostSet.flag(hostId)
}


/**
* Get collector proxy settings
* @return Map with proxy settings, empty map if proxy not set.
*/
Map getProxyInfo() {
    // Each property must be evaluated for null to determine whether to use collected value or fallback value
    // Elvis operator does not play nice with booleans
    // default to true in absence of property to use collectorProxy as determinant
    Boolean deviceProxy = hostProps.get("proxy.enable")?.toBoolean()
    deviceProxy = (deviceProxy != null) ? deviceProxy : true  
    // if settings are not present, value should be false
    Boolean collectorProxy = Settings.getSetting("proxy.enable")?.toBoolean()
    collectorProxy = (collectorProxy != null) ? collectorProxy : false  

    Map proxyInfo = [:]
    
    if (deviceProxy && collectorProxy) {
        proxyInfo = [
            enabled : true,
            host : hostProps.get("proxy.host") ?: Settings.getSetting("proxy.host"),
            port : hostProps.get("proxy.port") ?: Settings.getSetting("proxy.port") ?: 3128,
            user : Settings.getSetting("proxy.user"),
            pass : Settings.getSetting("proxy.pass")
        ]
    
        proxyInfo["proxy"] = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyInfo.host, proxyInfo.port.toInteger()))
    }

    return proxyInfo
}

 

2 Replies

  • Nice.

    My one comment would be, you should remove this line:

    // If script gets to this point, collector should consider this device alive
    keepAlive(hostProps)

    As the script is wholly interacting with the API, successful execution does not prove the resource is alive, and indeed, if the resource is down, this line will prevent LM from marking it as dead.