#!/usr/bin/env pmpython
#
# Copyright (c) 2021 Red Hat.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#

""" PCP MongoDB Performance Metrics Domain Agent """

# pylint: disable=bad-continuation,line-too-long,too-many-lines
# pylint: disable=bare-except,broad-except

import os
import sys
import pymongo
try:
    import configparser
except ImportError:
    import ConfigParser as configparser
from ctypes import c_int
from pcp.pmapi import pmUnits, pmContext
from pcp.pmda import PMDA, pmdaMetric, pmdaIndom
import cpmapi as c_api

class MONGODBPMDA(PMDA):
    """
    PMDA class for MongoDB performance metrics
    """

    # list indicies for metrics and indoms
    INDOM_META = 0
    INDOM_HELP = 1
    METRIC_CMD = 0
    METRIC_NAME = 1
    METRIC_META = 2
    METRIC_HELP = 3
    METRIC_PATH = 4 # optional, when name cannot be used to find value

    def PM_ID(self, cluster, item):
        """ pmid from PMDA domain, cluster and item """
        return PMDA.pmid(cluster, item)

    def INDOM_ID(self, serial):
        """ indom id from PMDA domain and serial """
        return PMDA.indom(serial)

    def __init__(self, name, domain):
        """ constructor """
        PMDA.__init__(self, name, domain)
        self.verbose = False # True for very verbose logging
        self.debug("__init__", "entered")

        zeroUnits = pmUnits(0, 0, 0, 0, 0, 0)
        countUnits = pmUnits(0, 0, 1, 0, 0, c_api.PM_COUNT_ONE)
        msecUnits = pmUnits(0, 1, 0, 0, c_api.PM_TIME_MSEC, 0)
        usecUnits = pmUnits(0, 1, 0, 0, c_api.PM_TIME_USEC, 0)
        byteUnits = pmUnits(1, 0, 0, c_api.PM_SPACE_BYTE, 0, 0)
        kbyteUnits = pmUnits(1, 0, 0, c_api.PM_SPACE_KBYTE, 0, 0)

        # Indom dict
        # indomid : [command, pmdaIndom, oneline, text]
        self.indoms = {
            self.INDOM_ID(0) : [pmdaIndom(self.INDOM_ID(0), {}), 'set of databases'],
        }

        # Metrics dict
        # pmid : [ pmns, pmdaMetric, oneline, text ]
        self.metrics = {
            # serverStatus command clusters
            self.PM_ID(0, 1) : ['serverStatus', 'pid', pmdaMetric(self.PM_ID(0, 1), c_api.PM_TYPE_U32, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT, zeroUnits), 'Process identifier for MongoDB server'],
            self.PM_ID(0, 2) : ['serverStatus', 'version', pmdaMetric(self.PM_ID(0, 2), c_api.PM_TYPE_STRING, c_api.PM_INDOM_NULL, c_api.PM_SEM_DISCRETE, zeroUnits), 'Version number for MongoDB installation'],
            self.PM_ID(0, 3) : ['serverStatus', 'uptime', pmdaMetric(self.PM_ID(0, 3), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT, msecUnits), 'Duration for which MongoDB server has been up', 'uptimeMillis'],
            self.PM_ID(1, 0) : ['serverStatus', 'connections.current', pmdaMetric(self.PM_ID(1, 0), c_api.PM_TYPE_U32, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT, zeroUnits), 'Number of open client connections'],
            self.PM_ID(1, 1) : ['serverStatus', 'connections.available', pmdaMetric(self.PM_ID(1, 1), c_api.PM_TYPE_U32, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT, zeroUnits), 'Number of available connections'],
            self.PM_ID(1, 2) : ['serverStatus', 'connections.totalCreated', pmdaMetric(self.PM_ID(1, 2), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of connections created'],
            self.PM_ID(1, 3) : ['serverStatus', 'connections.active', pmdaMetric(self.PM_ID(1, 3), c_api.PM_TYPE_U32, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT, zeroUnits), 'Number of active connections'],
            self.PM_ID(2, 0) : ['serverStatus', 'memory.resident', pmdaMetric(self.PM_ID(2, 0), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT, kbyteUnits), 'Resident memory used by MongoDB', 'mem.resident'],
            self.PM_ID(2, 1) : ['serverStatus', 'memory.virtual', pmdaMetric(self.PM_ID(2, 1), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT, kbyteUnits), 'Virtual memory used by MongoDB', 'mem.virtual'],
            self.PM_ID(3, 0) : ['serverStatus', 'process.utime', pmdaMetric(self.PM_ID(3, 0), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, usecUnits), 'Time spent running in user mode by MongoDB', 'extra_info.user_time_us'],
            self.PM_ID(3, 1) : ['serverStatus', 'process.stime', pmdaMetric(self.PM_ID(3, 1), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, usecUnits), 'Time spent running in jernel mode by MongoDB', 'extra_info.system_time_us'],
            self.PM_ID(3, 2) : ['serverStatus', 'process.rss_max', pmdaMetric(self.PM_ID(3, 2), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT, kbyteUnits), 'Maximum resident set size of the MongoDB server', 'extra_info.page_faults'],
            self.PM_ID(4, 0) : ['serverStatus', 'storageEngine.name', pmdaMetric(self.PM_ID(4, 0), c_api.PM_TYPE_STRING, c_api.PM_INDOM_NULL, c_api.PM_SEM_DISCRETE, zeroUnits), 'Name of the storage engine in use by MongoDB'],
            self.PM_ID(5, 0) : ['serverStatus', 'opcounters.query', pmdaMetric(self.PM_ID(5, 0), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of query operations performed'],
            self.PM_ID(5, 1) : ['serverStatus', 'opcounters.insert', pmdaMetric(self.PM_ID(5, 1), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of insert operations performed'],
            self.PM_ID(5, 2) : ['serverStatus', 'opcounters.update', pmdaMetric(self.PM_ID(5, 2), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of update operations performed'],
            self.PM_ID(5, 3) : ['serverStatus', 'opcounters.delete', pmdaMetric(self.PM_ID(5, 3), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of delete operations performed'],
            self.PM_ID(5, 4) : ['serverStatus', 'opcounters.getmore', pmdaMetric(self.PM_ID(5, 4), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of getmore operations performed'],
            self.PM_ID(5, 5) : ['serverStatus', 'opcounters.command', pmdaMetric(self.PM_ID(5, 5), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of command operations performed'],
            self.PM_ID(6, 0) : ['serverStatus', 'opLatencies.reads.latency', pmdaMetric(self.PM_ID(6, 0), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, usecUnits), 'Cumulative latency of read operations'],
            self.PM_ID(6, 1) : ['serverStatus', 'opLatencies.reads.ops', pmdaMetric(self.PM_ID(6, 1), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Cumulative count of read operations'],
            self.PM_ID(6, 2) : ['serverStatus', 'opLatencies.writes.latency', pmdaMetric(self.PM_ID(6, 2), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, usecUnits), 'Cumulative latency of write operations'],
            self.PM_ID(6, 3) : ['serverStatus', 'opLatencies.writes.ops', pmdaMetric(self.PM_ID(6, 3), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Cumulative count of write operations'],
            self.PM_ID(6, 4) : ['serverStatus', 'opLatencies.commands.latency', pmdaMetric(self.PM_ID(6, 4), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, usecUnits), 'Cumulative latency of command operations'],
            self.PM_ID(6, 5) : ['serverStatus', 'opLatencies.commands.ops', pmdaMetric(self.PM_ID(6, 5), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Cumulative count of command operations'],
            self.PM_ID(6, 6) : ['serverStatus', 'opLatencies.transactions.latency', pmdaMetric(self.PM_ID(6, 6), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, usecUnits), 'Cumulative latency of transactions'],
            self.PM_ID(6, 7) : ['serverStatus', 'opLatencies.transactions.ops', pmdaMetric(self.PM_ID(6, 7), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Cumulative count of transactions'],
            self.PM_ID(7, 0) : ['serverStatus', 'globalLock.currentQueue.total', pmdaMetric(self.PM_ID(7, 0), c_api.PM_TYPE_U32, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT, zeroUnits), 'Total globalLock current queue clients'],
            self.PM_ID(7, 1) : ['serverStatus', 'globalLock.currentQueue.readers', pmdaMetric(self.PM_ID(7, 1), c_api.PM_TYPE_U32, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT, zeroUnits), 'Total globalLock current queue reader clients'],
            self.PM_ID(7, 2) : ['serverStatus', 'globalLock.currentQueue.writers', pmdaMetric(self.PM_ID(7, 2), c_api.PM_TYPE_U32, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT, zeroUnits), 'Total globalLock current queue writer clients'],
            self.PM_ID(8, 0) : ['serverStatus', 'flowControl.timeAcquiringMicros', pmdaMetric(self.PM_ID(8, 0), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, usecUnits), 'Time spent acquiring flow control'],
            self.PM_ID(9, 0) : ['serverStatus', 'network.bytesIn', pmdaMetric(self.PM_ID(9, 0), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, byteUnits), 'Total logical bytes received from network clients'],
            self.PM_ID(9, 1) : ['serverStatus', 'network.bytesOut', pmdaMetric(self.PM_ID(9, 1), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, byteUnits), 'Total logical bytes sent to network clients'],
            self.PM_ID(9, 2) : ['serverStatus', 'network.physicalBytesIn', pmdaMetric(self.PM_ID(9, 2), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, byteUnits), 'Total bytes received from network clients'],
            self.PM_ID(9, 3) : ['serverStatus', 'network.physicalBytesOut', pmdaMetric(self.PM_ID(9, 3), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, byteUnits), 'Total physical bytes sent to network clients'],
            self.PM_ID(9, 4) : ['serverStatus', 'network.numSlowDNSOperations', pmdaMetric(self.PM_ID(9, 4), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of slow DNS operations'],
            self.PM_ID(9, 5) : ['serverStatus', 'network.numSlowSSLOperations', pmdaMetric(self.PM_ID(9, 5), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of slow SSL/TLS operations'],
            self.PM_ID(9, 6) : ['serverStatus', 'network.numRequests', pmdaMetric(self.PM_ID(9, 6), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of total network requests'],
            self.PM_ID(10, 0) : ['serverStatus', 'network.compression.snappy.compressor.bytesIn', pmdaMetric(self.PM_ID(10, 0), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of snappy-compressed network traffic in'],
            self.PM_ID(10, 1) : ['serverStatus', 'network.compression.snappy.compressor.bytesOut', pmdaMetric(self.PM_ID(10, 1), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of snappy-compressed network traffic out'],
            self.PM_ID(10, 2) : ['serverStatus', 'network.compression.snappy.decompressor.bytesIn', pmdaMetric(self.PM_ID(10, 2), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of snappy-decompressed network traffic in'],
            self.PM_ID(10, 3) : ['serverStatus', 'network.compression.snappy.decompressor.bytesOut', pmdaMetric(self.PM_ID(10, 3), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of snappy-compressed network traffic out'],
            self.PM_ID(11, 0) : ['serverStatus', 'network.compression.zstd.compressor.bytesIn', pmdaMetric(self.PM_ID(11, 0), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of zstd-compressed network traffic in'],
            self.PM_ID(11, 1) : ['serverStatus', 'network.compression.zstd.compressor.bytesOut', pmdaMetric(self.PM_ID(11, 1), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of zstd-compressed network traffic out'],
            self.PM_ID(11, 2) : ['serverStatus', 'network.compression.zstd.decompressor.bytesIn', pmdaMetric(self.PM_ID(11, 2), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of zstd-decompressed network traffic in'],
            self.PM_ID(11, 3) : ['serverStatus', 'network.compression.zstd.decompressor.bytesOut', pmdaMetric(self.PM_ID(11, 3), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of zstd-compressed network traffic out'],
            self.PM_ID(12, 0) : ['serverStatus', 'network.compression.zlib.compressor.bytesIn', pmdaMetric(self.PM_ID(12, 0), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of zlib-compressed network traffic in'],
            self.PM_ID(12, 1) : ['serverStatus', 'network.compression.zlib.compressor.bytesOut', pmdaMetric(self.PM_ID(12, 1), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of zlib-compressed network traffic out'],
            self.PM_ID(12, 2) : ['serverStatus', 'network.compression.zlib.decompressor.bytesIn', pmdaMetric(self.PM_ID(12, 2), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of zlib-decompressed network traffic in'],
            self.PM_ID(12, 3) : ['serverStatus', 'network.compression.zlib.decompressor.bytesOut', pmdaMetric(self.PM_ID(12, 3), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of zlib-compressed network traffic out'],
            self.PM_ID(13, 0) : ['serverStatus', 'transactions.currentActive', pmdaMetric(self.PM_ID(13, 0), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT, zeroUnits), 'Number of active MongoDB transactions'],
            self.PM_ID(14, 0) : ['serverStatus', 'wiredTiger.cache.bytes.current', pmdaMetric(self.PM_ID(14, 0), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT, byteUnits), 'Number of bytes cached by the wiredTiger engine', 'wiredTiger.cache.bytes currently in the cache'],
            self.PM_ID(14, 1) : ['serverStatus', 'wiredTiger.cache.bytes.read', pmdaMetric(self.PM_ID(14, 1), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT, byteUnits), 'Number of read bytes cached by the wiredTiger engine', 'wiredTiger.cache.bytes read into cache'],
            self.PM_ID(14, 2) : ['serverStatus', 'wiredTiger.cache.bytes.written', pmdaMetric(self.PM_ID(14, 2), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT, byteUnits), 'Number of cached bytes written by the wiredTiger engine', 'wiredTiger.cache.bytes written from cache'],
            self.PM_ID(14, 3) : ['serverStatus', 'wiredTiger.cache.bytes.dirty', pmdaMetric(self.PM_ID(14, 3), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, byteUnits), 'Total dirty bytes cached by the wiredTiger engine', 'wiredTiger.cache.bytes dirty in the cache cumulative'],
            self.PM_ID(14, 4) : ['serverStatus', 'wiredTiger.cache.bytes.maximum', pmdaMetric(self.PM_ID(14, 4), c_api.PM_TYPE_DOUBLE, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT, byteUnits), 'Maximum bytes configured for the wiredTiger engine cache', 'wiredTiger.cache.maximum bytes configured'],
            self.PM_ID(15, 0) : ['serverStatus', 'wiredTiger.concurrentTransactions.write.out', pmdaMetric(self.PM_ID(15, 0), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Concurrent transaction writes out by the wiredTiger engine'],
            self.PM_ID(15, 1) : ['serverStatus', 'wiredTiger.concurrentTransactions.read.out', pmdaMetric(self.PM_ID(15, 1), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Concurrent transaction reads out by the wiredTiger engine'],
            self.PM_ID(16, 0) : ['serverStatus', 'metrics.cursor.open.noTimeout', pmdaMetric(self.PM_ID(16, 0), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of cursor opens with no timeout'],
            self.PM_ID(16, 1) : ['serverStatus', 'metrics.cursor.open.pinned', pmdaMetric(self.PM_ID(16, 1), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of cursor opens that were pinned'],
            self.PM_ID(16, 2) : ['serverStatus', 'metrics.cursor.open.total', pmdaMetric(self.PM_ID(16, 2), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of cursor opens'],
            self.PM_ID(17, 0) : ['serverStatus', 'metrics.document.deleted', pmdaMetric(self.PM_ID(17, 0), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of documents deleted'],
            self.PM_ID(17, 1) : ['serverStatus', 'metrics.document.inserted', pmdaMetric(self.PM_ID(17, 1), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of documents inserted'],
            self.PM_ID(17, 2) : ['serverStatus', 'metrics.document.returned', pmdaMetric(self.PM_ID(17, 2), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of documents returned'],
            self.PM_ID(17, 3) : ['serverStatus', 'metrics.document.updated', pmdaMetric(self.PM_ID(17, 3), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of documents updated'],
            self.PM_ID(18, 0) : ['serverStatus', 'metrics.queryExecutor.scanned', pmdaMetric(self.PM_ID(18, 0), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of query executor scans'],
            self.PM_ID(18, 1) : ['serverStatus', 'metrics.queryExecutor.scannedObjects', pmdaMetric(self.PM_ID(18, 1), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, countUnits), 'Count of query executor object scans'],
            self.PM_ID(19, 0) : ['serverStatus', 'wiredTiger.transaction.last_checkpoint_time', pmdaMetric(self.PM_ID(19, 0), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT, msecUnits), 'Most recent wiredTiger transaction checkpoint time', 'wiredTiger.transaction.transaction checkpoint most recent time (msecs)'],
            self.PM_ID(19, 1) : ['serverStatus', 'wiredTiger.transaction.total_time', pmdaMetric(self.PM_ID(19, 1), c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, msecUnits), 'Total time spent performing wiredTiger checkpoint transactions', 'wiredTiger.transaction.transaction checkpoint total time (msecs)'],

            # dbStats command clusters
            self.PM_ID(100, 0) : ['dbStats', 'dataSize', pmdaMetric(self.PM_ID(100, 0), c_api.PM_TYPE_DOUBLE, self.INDOM_ID(0), c_api.PM_SEM_INSTANT, byteUnits), 'Per-database data size in bytes', 'dataSize'],
            self.PM_ID(100, 1) : ['dbStats', 'indexSize', pmdaMetric(self.PM_ID(100, 1), c_api.PM_TYPE_DOUBLE, self.INDOM_ID(0), c_api.PM_SEM_INSTANT, byteUnits), 'Per-database index size in bytes', 'indexSize'],
            self.PM_ID(100, 2) : ['dbStats', 'storageSize', pmdaMetric(self.PM_ID(100, 2), c_api.PM_TYPE_DOUBLE, self.INDOM_ID(0), c_api.PM_SEM_INSTANT, byteUnits), 'Per-database storage size in bytes', 'storageSize'],
        }

        # register all the indoms and metrics with the module
        self.setup_indoms()
        self.setup_metrics()

        # if this is the install process, we can bail out now
        if self.install():
            return

        pmdadir = os.path.join(pmContext.pmGetConfig('PCP_PMDAS_DIR'), name)
        conf_file = os.path.join(pmdadir, 'mongodb.conf')
        self.connect_pmcd()
        self.NAME = name

        # dicts to hold sampled data and indom lists
        self.status = {}
        self.dbnames = {}
        self.databases = {}

        # parse config for mongodb auth and server etc
        conf_vars = self.config(conf_file, ['authentication', 'connection'])
        self.setup_config(conf_vars)

        self.debug("__init__", "connection to MongoDB")
        self.setup_mongodb()

        self.set_label(self.mongodb_label)
        self.set_refresh(self.mongodb_refresh)
        self.set_instance(self.mongodb_instance)
        self.set_fetch_callback(self.mongodb_fetch_callback)
        self.log("Setup complete")

    def mongodb_refresh_databases(self):
        self.debug("mongodb_refresh_databases", "entered")
        self.dbnames = {}
        indom = self.INDOM_ID(0)
        stats = self.mdb.command("listDatabases")
        for db in stats.get('databases'):
            instname = db.get('name')
            self.dbnames[instname] = c_int(1)
            self.debug("mongodb_refresh_databases",
                       "indom=0x%04x instname=%s" % (indom, instname))
        self.replace_indom(indom, self.dbnames)

    def mongodb_refresh(self, cluster):
        """ refresh individual clusters as needed """
        if cluster == 100:
            if not self.dbnames:
                self.mongodb_refresh_databases()
            for name in self.dbnames:
                self.debug("mongodb_refresh database", name)
                db = self.m.get_database(name)
                self.databases[name] = db.command("dbStats")
        else:
            self.status = self.mdb.command("serverStatus")

    def mongodb_instance(self, serial):
        """ called once per "instance" PDU """
        if serial == 0: # only one so far - the 'set of databases' indom
            self.mongodb_refresh_databases()

    def mongodb_label(self, ident, target):
        """ return JSONB labelset for identifier of given type, as a string """
        if target == c_api.PM_LABEL_INDOM:
            indom = ident
            if indom == self.INDOM_ID(0):
                ret = '"indom_name":"databases"'
        else: # no labels added for PM_LABEL_{DOMAIN,CLUSTER,ITEM}
            ret = '' # default is an empty labelset string
        return '{%s}' % ret

    def mongodb_value_map(self, json, name):
        """ descend into JSON document looking up a value for a path """
        split = name.split('.')
        value = json
        for p in split:
            value = value.get(p)
        return value

    def mongodb_fetch_callback(self, cluster, item, inst):
        """ Called for each instance of each fetched metric """
        self.debug("mongodb_fetch_callback",
                   "entered [%d.%d] inst=%d" % (cluster, item, inst))

        pmid = self.PM_ID(cluster, item)
        metric = self.metrics[pmid]
        if metric is None:
            return [c_api.PM_ERR_PMID, 0]
        name = path = metric[self.METRIC_NAME]
        indom = metric[self.METRIC_META].m_desc.indom
        if len(metric) == self.METRIC_PATH + 1:
            path = metric[self.METRIC_PATH]

        self.debug("mongodb_fetch_callback",
                   "%s pmid 0x%04x [%d.%d] inst=%d, indom=0x%04x" %
                   (name, pmid, cluster, item, inst, indom))

        # lookup the value for this metric[instance] in the appopriate document
        try:
            if indom == c_api.PM_INDOM_NULL:
                values = self.status
            else:
                instname = self.inst_name_lookup(indom, inst)
                self.debug("mongodb_fetch_callback",
                           "indom=0x%04x inst=%d instname=%s" %
                           (indom, inst, instname))
                if instname is None:
                    return [c_api.PM_ERR_INST, 0] # unknown instance for this pmid
                if indom == self.INDOM_ID(0):
                    values = self.databases[instname]
                else:
                    return [c_api.PM_ERR_INDOM, 0] # unexpected indom
            value = self.mongodb_value_map(values, path)
            self.debug("mongodb_fetch_callback", "%s pmid 0x%04x [%d.%d] inst=%d, value=%s" %
                    (name, pmid, cluster, item, inst, value))
        except KeyError:
            # no values available
            self.debug("mongodb_fetch_callback",
                       "no value for pmid=0x%04x name=%s inst=%d" %
                       (pmid, name, inst))
            return [c_api.PM_ERR_VALUE, 0]
        except:
            self.error("mongodb_fetch_callback",
                       "unexpected pmid=0x%04x name=%s error" % (pmid, name))
            return [c_api.PM_ERR_VALUE, 0]

        if value is None:
            if metric[self.METRIC_META].m_desc.type == c_api.PM_TYPE_STRING:
                value = ""
            else:
                value = 0
        return [value, 1]

    def setup_indoms(self):
        """ create indom table """
        for indom, data in self.indoms.items():
            self.add_indom(data[self.INDOM_META], data[self.INDOM_HELP])
            self.debug("setup_indoms: added indom 0x%04x: %s" % (indom, data[self.INDOM_HELP]))

    def setup_metrics(self):
        """ create metric table """
        for pmid, metric in self.metrics.items():
            pmns = 'mongodb.' + metric[self.METRIC_CMD] + '.'
            pmns += metric[self.METRIC_NAME]
            self.debug("Adding pmid 0x%04x %s" % (pmid, pmns))
            self.add_metric(pmns, metric[self.METRIC_META], metric[self.METRIC_HELP])

    def config(self, filename, sections):
        """ parse config file """
        # Python < 3.2 compat
        if sys.version_info[0] >= 3 and sys.version_info[1] >= 2:
            parser = configparser.ConfigParser()
        else:
            parser = configparser.SafeConfigParser()
        parser.read(filename)

        # get each requested section
        params = {}
        for section in sections:
            if parser.has_section(section):
                conf = parser.items(section)
                for param in conf:
                    params[section + '.' + param[0]] = param[1]
            else:
                raise Exception('Section %s not found in the %s file' %
                        (section, filename))
        return params # dict

    def setup_config(self, conf_vars):
        """ set local variables based on config file, or use defaults """
        try:
            self.host = conf_vars["connection.host"]
        except:
            self.host = "localhost"
        try:
            self.port = int(conf_vars["connection.port"])
        except:
            self.port = 27017
        try:
            self.username = conf_vars["authentication.username"]
        except:
            self.username = pmContext.pmGetConfig('PCP_USER')
        try:
            self.password = conf_vars["authentication.password"]
        except:
            self.password = None

    def setup_mongodb(self):
        """ connect to the MongoDB server """
        try:
            self.m = pymongo.MongoClient(self.host)
            self.mdb = self.m.admin
            if self.password:
                self.mdb.authenticate(self.username, self.password)
        except Exception as error: # pylint disable: broad-except
            self.error("setup_mongodb", "cannot connect: %s" % error)
            return False
        self.log("Connected to MongoDB at %s:%d" % (self.host, self.port))
        return True

    def install(self):
        """ check whether running the Install process """
        if 'PCP_PYTHON_DOMAIN' in os.environ:
            return True
        elif 'PCP_PYTHON_PMNS' in os.environ:
            return True
        return False

    def error(self, function, message):
        """ print diagnostic with enforced function name """
        self.err(function + ": " + message)

    def debug(self, function, message=None):
        """ print diagnostic message only if verbose logging is enabled """
        if self.verbose:
            if message:
                self.dbg(function + ": " + message)
            else:
                self.dbg(function)

if __name__ == "__main__":
    pmda = MONGODBPMDA("mongodb", 35)
    pmda.run()
