from typing import Dict import re import json from cpmapi import PM_SEM_INSTANT, PM_SEM_COUNTER, PM_TYPE_U64, PM_TYPE_STRING from .models import RuntimeInfo, Script, BPFtraceError, VariableDefinition, MetricType class BPFtraceMessageType: """BPFtrace JSON output types""" AttachedProbes = 'attached_probes' Map = 'map' Hist = 'hist' Printf = 'printf' Time = 'time' def parse_code(script: Script): """parse bpftrace script (read variable semantics, add continuous output)""" found_metadata = re.findall(r'^//\s*([\w\-]+)' + # key r'(?:\s*:\s*(.+?)\s*)?$', # optional value script.code, re.MULTILINE) for key, val in found_metadata: if key == 'name': if re.match(r'^[a-zA-Z_]\w+$', val): script.metadata.name = val else: raise BPFtraceError(f"invalid value '{val}' for {key}: " f"must contain only alphanumeric characters and " f"start with a letter") elif key == 'include': script.metadata.include = val.split(',') elif key == 'table-retain-lines': if val.isdigit(): script.metadata.table_retain_lines = int(val) else: raise BPFtraceError(f"invalid value '{val}' for {key}, " f"must be numeric") elif key == 'custom-output-block': script.metadata.custom_output_block = True all_variables = re.findall(r'(@.*?)' + # variable r'(?:\[(.+?)\])?' + # optional map key r'\s*=\s*' + # assignment r'(?:([a-zA-Z]\w*)\s*\()?', # optional function script.code) for var, key, func in all_variables: if script.metadata.include is not None and var not in script.metadata.include: continue vardef = VariableDefinition(single=True, semantics=PM_SEM_INSTANT, datatype=PM_TYPE_U64, metrictype=None) if func in ['hist', 'lhist']: if key: raise BPFtraceError("every histogram needs to be in a separate variable") vardef.single = False vardef.semantics = PM_SEM_COUNTER vardef.metrictype = MetricType.Histogram elif func == 'count': vardef.single = not bool(key) vardef.semantics = PM_SEM_COUNTER if re.search(r'(^|,)[ku]stack($|,)', key): vardef.metrictype = MetricType.Stacks else: vardef.single = not bool(key) vardef.semantics = PM_SEM_INSTANT script.variables[var] = vardef output_fns = re.search(r'(printf|time)\s*\(', script.code) if output_fns and (script.metadata.include is None or '@output' in script.metadata.include): if '@output' in script.variables: raise BPFtraceError("output from printf(), time() etc. will be stored in @output. " "please rename the existing @output variable or remove any " "calls to any function which produces output") script.variables['@output'] = VariableDefinition(single=True, semantics=PM_SEM_INSTANT, datatype=PM_TYPE_STRING, metrictype=MetricType.Output) if not script.variables: raise BPFtraceError("no bpftrace variables or printf statements found, please include " "at least one variable or printf statement in your script") if script.metadata.name: script.persistent = True return script def table_retain_lines(script: Script): """cut table lines and keep first line (header)""" output = script.state.data['@output'] newlines = [] for i, c in enumerate(output): if c == '\n': newlines.append(i + 1) # e.g. if we found one NL, we have 2 lines if len(newlines) + 1 > 1 + script.metadata.table_retain_lines: # special handling if last character is a NL ignore_last_nl = 1 if output[-1] == '\n' else 0 # pylint: disable=invalid-unary-operand-type start_content_at = newlines[-script.metadata.table_retain_lines - ignore_last_nl] script.state.data['@output'] = output[:newlines[0]] + output[start_content_at:] def process_bpftrace_output_obj(runtime_info: RuntimeInfo, script: Script, obj: Dict): """process a single JSON object from bpftrace output""" if obj['type'] == BPFtraceMessageType.AttachedProbes: if runtime_info.bpftrace_version <= (0, 9, 2): script.state.probes = obj['probes'] else: # https://github.com/iovisor/bpftrace/commit/9d1269b script.state.probes = obj['data']['probes'] elif obj['type'] == BPFtraceMessageType.Map: script.state.data.update(obj['data']) elif obj['type'] == BPFtraceMessageType.Hist: for k, v in obj['data'].items(): script.state.data[k] = { '{}-{}'.format(bucket.get('min', '-inf'), bucket.get('max', 'inf')): bucket['count'] for bucket in v } elif obj['type'] in [BPFtraceMessageType.Printf, BPFtraceMessageType.Time]: if runtime_info.bpftrace_version <= (0, 9, 2): script.state.data['@output'] = script.state.data.get('@output', '') + obj['msg'] else: # https://github.com/iovisor/bpftrace/commit/9d1269b script.state.data['@output'] = script.state.data.get('@output', '') + obj['data'] if script.metadata.table_retain_lines is not None: table_retain_lines(script) def process_bpftrace_output(runtime_info: RuntimeInfo, script: Script, line: str): if runtime_info.bpftrace_version <= (0, 9, 2) and '": }' in line: # invalid JSON, fixed in https://github.com/iovisor/bpftrace/commit/348975b return if not line or line.isspace(): return obj = json.loads(line) process_bpftrace_output_obj(runtime_info, script, obj)