def _collect_plan_for_statement(self, row): # Plans have several important signatures to tag events with: # - `plan_signature` - hash computed from the normalized JSON plan to group identical plan trees # - `resource_hash` - hash computed off the raw sql text to match apm resources # - `query_signature` - hash computed from the digest text to match query metrics try: obfuscated_statement = datadog_agent.obfuscate_sql(row['sql_text']) obfuscated_digest_text = datadog_agent.obfuscate_sql( row['digest_text']) except Exception: # do not log the raw sql_text to avoid leaking sensitive data into logs. digest_text is safe as parameters # are obfuscated by the database self._log.debug("Failed to obfuscate statement: %s", row['digest_text']) self._check.count("dd.mysql.statement_samples.error", 1, tags=self._tags + ["error:sql-obfuscate"]) return None apm_resource_hash = compute_sql_signature(obfuscated_statement) query_signature = compute_sql_signature(obfuscated_digest_text) query_cache_key = (row['current_schema'], query_signature) if query_cache_key in self._explained_statements_cache: return None self._explained_statements_cache[query_cache_key] = True plan = None with closing(self._get_db_connection().cursor()) as cursor: try: plan = self._explain_statement(cursor, row['sql_text'], row['current_schema'], obfuscated_statement) except Exception as e: self._check.count("dd.mysql.statement_samples.error", 1, tags=self._tags + ["error:explain-{}".format(type(e))]) self._log.exception("Failed to explain statement: %s", obfuscated_statement) normalized_plan, obfuscated_plan, plan_signature, plan_cost = None, None, None, None if plan: normalized_plan = datadog_agent.obfuscate_sql_exec_plan( plan, normalize=True) if plan else None obfuscated_plan = datadog_agent.obfuscate_sql_exec_plan(plan) plan_signature = compute_exec_plan_signature(normalized_plan) plan_cost = self._parse_execution_plan_cost(plan) query_plan_cache_key = (query_cache_key, plan_signature) if query_plan_cache_key not in self._seen_samples_cache: self._seen_samples_cache[query_plan_cache_key] = True return { "timestamp": row["timer_end_time_s"] * 1000, "host": self._db_hostname, "service": self._service, "ddsource": "mysql", "ddtags": self._tags_str, "duration": row['timer_wait_ns'], "network": { "client": { "ip": row.get('processlist_host', None), } }, "db": { "instance": row['current_schema'], "plan": { "definition": obfuscated_plan, "cost": plan_cost, "signature": plan_signature }, "query_signature": query_signature, "resource_hash": apm_resource_hash, "statement": obfuscated_statement, }, 'mysql': { k: v for k, v in row.items() if k not in EVENTS_STATEMENTS_SAMPLE_EXCLUDE_KEYS }, }
def _collect_plan_for_statement(self, row): try: obfuscated_statement = datadog_agent.obfuscate_sql(row['query']) except Exception as e: self._log.debug("Failed to obfuscate statement: %s", e) self._check.count("dd.postgres.statement_samples.error", 1, tags=self._tags + ["error:sql-obfuscate"]) return None # limit the rate of explains done to the database query_signature = compute_sql_signature(obfuscated_statement) if query_signature in self._explained_statements_cache: return None self._explained_statements_cache[query_signature] = True # Plans have several important signatures to tag events with. Note that for postgres, the # query_signature and resource_hash will be the same value. # - `plan_signature` - hash computed from the normalized JSON plan to group identical plan trees # - `resource_hash` - hash computed off the raw sql text to match apm resources # - `query_signature` - hash computed from the raw sql text to match query metrics plan_dict = self._run_explain(row['query'], obfuscated_statement) plan, normalized_plan, obfuscated_plan, plan_signature, plan_cost = None, None, None, None, None if plan_dict: plan = json.dumps(plan_dict) # if we're using the orjson implementation then json.dumps returns bytes plan = plan.decode('utf-8') if isinstance(plan, bytes) else plan normalized_plan = datadog_agent.obfuscate_sql_exec_plan(plan, normalize=True) obfuscated_plan = datadog_agent.obfuscate_sql_exec_plan(plan) plan_signature = compute_exec_plan_signature(normalized_plan) plan_cost = plan_dict.get('Plan', {}).get('Total Cost', 0.0) or 0.0 statement_plan_sig = (query_signature, plan_signature) if statement_plan_sig not in self._seen_samples_cache: self._seen_samples_cache[statement_plan_sig] = True event = { "host": self._db_hostname, "service": self._service, "ddsource": "postgres", "ddtags": self._tags_str, "network": { "client": { "ip": row.get('client_addr', None), "port": row.get('client_port', None), "hostname": row.get('client_hostname', None), } }, "db": { "instance": row.get('datname', None), "plan": {"definition": obfuscated_plan, "cost": plan_cost, "signature": plan_signature}, "query_signature": query_signature, "resource_hash": query_signature, "application": row.get('application_name', None), "user": row['usename'], "statement": obfuscated_statement, }, 'postgres': {k: v for k, v in row.items() if k not in pg_stat_activity_sample_exclude_keys}, } event['timestamp'] = time.time() * 1000 if row['state'] in {'idle', 'idle in transaction'}: if row['state_change'] and row['query_start']: event['duration'] = (row['state_change'] - row['query_start']).total_seconds() * 1e9 # If the transaction is idle then we have a more specific "end time" than the current time at # which we're collecting this event. According to the postgres docs, all of the timestamps in # pg_stat_activity are `timestamp with time zone` so the timezone should always be present. However, # if there is something wrong and it's missing then we can't use `state_change` for the timestamp # of the event else we risk the timestamp being significantly off and the event getting dropped # during ingestion. if row['state_change'].tzinfo: event['timestamp'] = get_timestamp(row['state_change']) * 1000 return event
def _collect_plan_for_statement(self, row): # Plans have several important signatures to tag events with: # - `plan_signature` - hash computed from the normalized JSON plan to group identical plan trees # - `resource_hash` - hash computed off the raw sql text to match apm resources # - `query_signature` - hash computed from the digest text to match query metrics try: statement = obfuscate_sql_with_metadata(row['sql_text'], self._obfuscate_options) statement_digest_text = obfuscate_sql_with_metadata( row['digest_text'], self._obfuscate_options) except Exception: # do not log the raw sql_text to avoid leaking sensitive data into logs. digest_text is safe as parameters # are obfuscated by the database self._log.debug("Failed to obfuscate statement: %s", row['digest_text']) self._check.count( "dd.mysql.query_samples.error", 1, tags=self._tags + ["error:sql-obfuscate"] + self._check._get_debug_tags(), hostname=self._check.resolved_hostname, ) return None obfuscated_statement = statement['query'] obfuscated_digest_text = statement_digest_text['query'] apm_resource_hash = compute_sql_signature(obfuscated_statement) query_signature = compute_sql_signature(obfuscated_digest_text) query_cache_key = (row['current_schema'], query_signature) if not self._explained_statements_ratelimiter.acquire(query_cache_key): return None with closing(self._get_db_connection().cursor()) as cursor: plan, error_states = self._explain_statement( cursor, row['sql_text'], row['current_schema'], obfuscated_statement, query_signature) collection_errors = [] if error_states: for state in error_states: error_tag = "error:explain-{}-{}".format( state.error_code, state.error_message) self._check.count( "dd.mysql.query_samples.error", 1, tags=self._tags + [error_tag] + self._check._get_debug_tags(), hostname=self._check.resolved_hostname, ) collection_errors.append({ 'strategy': state.strategy, 'code': state.error_code.value if state.error_code else None, 'message': state.error_message, }) normalized_plan, obfuscated_plan, plan_signature = None, None, None if plan: normalized_plan = datadog_agent.obfuscate_sql_exec_plan( plan, normalize=True) if plan else None obfuscated_plan = datadog_agent.obfuscate_sql_exec_plan(plan) plan_signature = compute_exec_plan_signature(normalized_plan) query_plan_cache_key = (query_cache_key, plan_signature) if self._seen_samples_ratelimiter.acquire(query_plan_cache_key): return { "timestamp": row["timer_end_time_s"] * 1000, "host": self._check.resolved_hostname, "ddagentversion": datadog_agent.get_version(), "ddsource": "mysql", "ddtags": self._tags_str, "duration": row['timer_wait_ns'], "network": { "client": { "ip": row.get('processlist_host', None), } }, "db": { "instance": row['current_schema'], "plan": { "definition": obfuscated_plan, "signature": plan_signature, "collection_errors": collection_errors if collection_errors else None, }, "query_signature": query_signature, "resource_hash": apm_resource_hash, "statement": obfuscated_statement, "metadata": { "tables": statement['metadata'].get('tables', None), "commands": statement['metadata'].get('commands', None), "comments": statement['metadata'].get('comments', None), }, "query_truncated": get_truncation_state(row['sql_text']).value, }, 'mysql': { k: v for k, v in row.items() if k not in EVENTS_STATEMENTS_SAMPLE_EXCLUDE_KEYS }, }
def _collect_plan_for_statement(self, row): try: obfuscated_statement = datadog_agent.obfuscate_sql(row['query']) except Exception as e: self._log.debug("Failed to obfuscate statement: %s", e) self._check.count("dd.postgres.statement_samples.error", 1, tags=self._tags + ["error:sql-obfuscate"]) return None # limit the rate of explains done to the database query_signature = compute_sql_signature(obfuscated_statement) if query_signature in self._explained_statements_cache: return None self._explained_statements_cache[query_signature] = True # Plans have several important signatures to tag events with. Note that for postgres, the # query_signature and resource_hash will be the same value. # - `plan_signature` - hash computed from the normalized JSON plan to group identical plan trees # - `resource_hash` - hash computed off the raw sql text to match apm resources # - `query_signature` - hash computed from the raw sql text to match query metrics plan_dict = self._run_explain(row['query'], obfuscated_statement) plan, normalized_plan, obfuscated_plan, plan_signature, plan_cost = None, None, None, None, None if plan_dict: plan = json.dumps(plan_dict) normalized_plan = datadog_agent.obfuscate_sql_exec_plan( plan, normalize=True) obfuscated_plan = datadog_agent.obfuscate_sql_exec_plan(plan) plan_signature = compute_exec_plan_signature(normalized_plan) plan_cost = plan_dict.get('Plan', {}).get('Total Cost', 0.0) or 0.0 statement_plan_sig = (query_signature, plan_signature) if statement_plan_sig not in self._seen_samples_cache: self._seen_samples_cache[statement_plan_sig] = True event = { "host": self._db_hostname, "service": self._service, "ddsource": "postgres", "ddtags": self._tags_str, "network": { "client": { "ip": row.get('client_addr', None), "port": row.get('client_port', None), "hostname": row.get('client_hostname', None), } }, "db": { "instance": row.get('datname', None), "plan": { "definition": obfuscated_plan, "cost": plan_cost, "signature": plan_signature }, "query_signature": query_signature, "resource_hash": query_signature, "application": row.get('application_name', None), "user": row['usename'], "statement": obfuscated_statement, }, 'postgres': { k: v for k, v in row.items() if k not in pg_stat_activity_sample_exclude_keys }, } if row['state'] in {'idle', 'idle in transaction'}: if row['state_change'] and row['query_start']: event['duration'] = ( row['state_change'] - row['query_start']).total_seconds() * 1e9 event['timestamp'] = time.mktime( row['state_change'].timetuple()) * 1000 else: event['timestamp'] = time.time() * 1000 return event
def _collect_plan_for_statement(self, row): # limit the rate of explains done to the database cache_key = (row['datname'], row['query_signature']) if not self._explained_statements_ratelimiter.acquire(cache_key): return None # Plans have several important signatures to tag events with. Note that for postgres, the # query_signature and resource_hash will be the same value. # - `plan_signature` - hash computed from the normalized JSON plan to group identical plan trees # - `resource_hash` - hash computed off the raw sql text to match apm resources # - `query_signature` - hash computed from the raw sql text to match query metrics plan_dict, explain_err_code, err_msg = self._run_and_track_explain( row['datname'], row['query'], row['statement'], row['query_signature']) collection_errors = None if explain_err_code: collection_errors = [{ 'code': explain_err_code.value, 'message': err_msg if err_msg else None }] plan, normalized_plan, obfuscated_plan, plan_signature = None, None, None, None if plan_dict: plan = json.dumps(plan_dict) # if we're using the orjson implementation then json.dumps returns bytes plan = plan.decode('utf-8') if isinstance(plan, bytes) else plan normalized_plan = datadog_agent.obfuscate_sql_exec_plan( plan, normalize=True) obfuscated_plan = datadog_agent.obfuscate_sql_exec_plan(plan) plan_signature = compute_exec_plan_signature(normalized_plan) statement_plan_sig = (row['query_signature'], plan_signature) if self._seen_samples_ratelimiter.acquire(statement_plan_sig): event = { "host": self._check.resolved_hostname, "ddagentversion": datadog_agent.get_version(), "ddsource": "postgres", "ddtags": ",".join(self._dbtags(row['datname'])), "timestamp": time.time() * 1000, "network": { "client": { "ip": row.get('client_addr', None), "port": row.get('client_port', None), "hostname": row.get('client_hostname', None), } }, "db": { "instance": row.get('datname', None), "plan": { "definition": obfuscated_plan, "signature": plan_signature, "collection_errors": collection_errors, }, "query_signature": row['query_signature'], "resource_hash": row['query_signature'], "application": row.get('application_name', None), "user": row['usename'], "statement": row['statement'], "metadata": { "tables": row['dd_tables'], "commands": row['dd_commands'], "comments": row['dd_comments'], }, "query_truncated": self._get_truncation_state( self._get_track_activity_query_size(), row['query']).value, }, 'postgres': { k: v for k, v in row.items() if k not in pg_stat_activity_sample_exclude_keys }, } if row['state'] in {'idle', 'idle in transaction'}: if row['state_change'] and row['query_start']: event['duration'] = ( row['state_change'] - row['query_start']).total_seconds() * 1e9 # If the transaction is idle then we have a more specific "end time" than the current time at # which we're collecting this event. According to the postgres docs, all of the timestamps in # pg_stat_activity are `timestamp with time zone` so the timezone should always be present. However, # if there is something wrong and it's missing then we can't use `state_change` for the timestamp # of the event else we risk the timestamp being significantly off and the event getting dropped # during ingestion. if row['state_change'].tzinfo: event['timestamp'] = get_timestamp( row['state_change']) * 1000 return event return None