PG-293: Add pg_stat_monitor.track GUC.
This new GUC allows users to select which statements are tracked by pg_stat_monitor: - 'top': Default, track only top level queries. - 'all': Track top along with sub/nested queries. - 'none': Disable query monitoring. To avoid redudancy, now users disable pg_stat_monitor by setting pg_stat_monitor.track = 'none', similar to pg_stat_statements. This new GUC is an enumeration, so the pg_stat_monitor_settings view was adjusted to add a new column 'options' which lists the possible values for the field. The "value" and "default_value" columns in the pg_stat_monitor_settings view was adjusted to be of type text, so we can better display the enumeration values. Also the boolean types now have their values displayed as either 'yes' or 'no' to easily distinguish them from the integer types.pull/157/head
parent
6f7f44b744
commit
b6838049b6
50
guc.c
50
guc.c
|
@ -21,6 +21,7 @@
|
|||
GucVariable conf[MAX_SETTINGS];
|
||||
static void DefineIntGUC(GucVariable *conf);
|
||||
static void DefineBoolGUC(GucVariable *conf);
|
||||
static void DefineEnumGUC(GucVariable *conf, const struct config_enum_entry *options);
|
||||
|
||||
/*
|
||||
* Define (or redefine) custom GUC variables.
|
||||
|
@ -29,6 +30,7 @@ void
|
|||
init_guc(void)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
conf[i] = (GucVariable) {
|
||||
.guc_name = "pg_stat_monitor.pgsm_max",
|
||||
.guc_desc = "Sets the maximum size of shared memory in (MB) used for statement's metadata tracked by pg_stat_monitor.",
|
||||
|
@ -53,18 +55,6 @@ init_guc(void)
|
|||
};
|
||||
DefineIntGUC(&conf[i++]);
|
||||
|
||||
conf[i] = (GucVariable) {
|
||||
.guc_name = "pg_stat_monitor.pgsm_enable",
|
||||
.guc_desc = "Enable/Disable statistics collector.",
|
||||
.guc_default = 1,
|
||||
.guc_min = 0,
|
||||
.guc_max = 0,
|
||||
.guc_restart = false,
|
||||
.guc_unit = 0,
|
||||
.guc_value = &PGSM_ENABLED
|
||||
};
|
||||
DefineBoolGUC(&conf[i++]);
|
||||
|
||||
conf[i] = (GucVariable) {
|
||||
.guc_name = "pg_stat_monitor.pgsm_track_utility",
|
||||
.guc_desc = "Selects whether utility commands are tracked.",
|
||||
|
@ -185,6 +175,21 @@ init_guc(void)
|
|||
};
|
||||
DefineBoolGUC(&conf[i++]);
|
||||
|
||||
conf[i] = (GucVariable) {
|
||||
.guc_name = "pg_stat_monitor.track",
|
||||
.guc_desc = "Selects which statements are tracked by pg_stat_monitor.",
|
||||
.n_options = 3,
|
||||
.guc_default = PGSM_TRACK_TOP,
|
||||
.guc_min = PSGM_TRACK_NONE,
|
||||
.guc_max = PGSM_TRACK_ALL,
|
||||
.guc_restart = false,
|
||||
.guc_unit = 0,
|
||||
.guc_value = &PGSM_TRACK
|
||||
};
|
||||
for (int j = 0; j < conf[i].n_options; ++j) {
|
||||
strlcpy(conf[i].guc_options[j], track_options[j].name, sizeof(conf[i].guc_options[j]));
|
||||
}
|
||||
DefineEnumGUC(&conf[i++], track_options);
|
||||
|
||||
#if PG_VERSION_NUM >= 130000
|
||||
conf[i] = (GucVariable) {
|
||||
|
@ -204,6 +209,7 @@ init_guc(void)
|
|||
static void
|
||||
DefineIntGUC(GucVariable *conf)
|
||||
{
|
||||
conf->type = PGC_INT;
|
||||
DefineCustomIntVariable(conf->guc_name,
|
||||
conf->guc_desc,
|
||||
NULL,
|
||||
|
@ -220,6 +226,7 @@ DefineIntGUC(GucVariable *conf)
|
|||
static void
|
||||
DefineBoolGUC(GucVariable *conf)
|
||||
{
|
||||
conf->type = PGC_BOOL;
|
||||
DefineCustomBoolVariable(conf->guc_name,
|
||||
conf->guc_desc,
|
||||
NULL,
|
||||
|
@ -232,9 +239,26 @@ DefineBoolGUC(GucVariable *conf)
|
|||
NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
DefineEnumGUC(GucVariable *conf, const struct config_enum_entry *options)
|
||||
{
|
||||
conf->type = PGC_ENUM;
|
||||
DefineCustomEnumVariable(conf->guc_name,
|
||||
conf->guc_desc,
|
||||
NULL,
|
||||
conf->guc_value,
|
||||
PGSM_TRACK_TOP,
|
||||
options,
|
||||
conf->guc_restart ? PGC_POSTMASTER : PGC_USERSET,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL);
|
||||
}
|
||||
|
||||
GucVariable*
|
||||
get_conf(int i)
|
||||
{
|
||||
return &conf[i];
|
||||
return &conf[i];
|
||||
}
|
||||
|
||||
|
|
|
@ -116,12 +116,13 @@ LANGUAGE SQL PARALLEL SAFE;
|
|||
|
||||
CREATE FUNCTION pg_stat_monitor_settings(
|
||||
OUT name text,
|
||||
OUT value INTEGER,
|
||||
OUT default_value INTEGER,
|
||||
OUT value text,
|
||||
OUT default_value text,
|
||||
OUT description text,
|
||||
OUT minimum INTEGER,
|
||||
OUT maximum INTEGER,
|
||||
OUT restart INTEGER
|
||||
OUT options text,
|
||||
OUT restart text
|
||||
)
|
||||
RETURNS SETOF record
|
||||
AS 'MODULE_PATHNAME', 'pg_stat_monitor_settings'
|
||||
|
@ -134,6 +135,7 @@ CREATE VIEW pg_stat_monitor_settings AS SELECT
|
|||
description,
|
||||
minimum,
|
||||
maximum,
|
||||
options,
|
||||
restart
|
||||
FROM pg_stat_monitor_settings();
|
||||
|
||||
|
|
|
@ -46,6 +46,12 @@ do \
|
|||
strlcpy((char *)_str_dst[i], _str_src[i], _len2); \
|
||||
}while(0)
|
||||
|
||||
#define pgsm_enabled(level) \
|
||||
(!IsParallelWorker() && \
|
||||
(PGSM_TRACK == PGSM_TRACK_ALL || \
|
||||
(PGSM_TRACK == PGSM_TRACK_TOP && (level) == 0)))
|
||||
|
||||
|
||||
/*---- Initicalization Function Declarations ----*/
|
||||
void _PG_init(void);
|
||||
void _PG_fini(void);
|
||||
|
@ -53,7 +59,7 @@ void _PG_fini(void);
|
|||
/*---- Local variables ----*/
|
||||
|
||||
/* Current nesting depth of ExecutorRun+ProcessUtility calls */
|
||||
static int nested_level = 0;
|
||||
static int exec_nested_level = 0;
|
||||
#if PG_VERSION_NUM >= 130000
|
||||
static int plan_nested_level = 0;
|
||||
#endif
|
||||
|
@ -356,7 +362,7 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)
|
|||
if (!IsSystemInitialized())
|
||||
return;
|
||||
|
||||
if (IsParallelWorker())
|
||||
if (!pgsm_enabled(exec_nested_level))
|
||||
return;
|
||||
|
||||
/*
|
||||
|
@ -421,7 +427,7 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query)
|
|||
if (!IsSystemInitialized())
|
||||
return;
|
||||
|
||||
if (IsParallelWorker())
|
||||
if (!pgsm_enabled(exec_nested_level))
|
||||
return;
|
||||
|
||||
/*
|
||||
|
@ -489,15 +495,13 @@ pgss_ExecutorStart(QueryDesc *queryDesc, int eflags)
|
|||
else
|
||||
standard_ExecutorStart(queryDesc, eflags);
|
||||
|
||||
if (IsParallelWorker())
|
||||
return;
|
||||
|
||||
/*
|
||||
* If query has queryId zero, don't track it. This prevents double
|
||||
* counting of optimizable statements that are directly contained in
|
||||
* utility statements.
|
||||
*/
|
||||
if (PGSM_ENABLED && queryDesc->plannedstmt->queryId != UINT64CONST(0))
|
||||
if (pgsm_enabled(exec_nested_level) &&
|
||||
queryDesc->plannedstmt->queryId != UINT64CONST(0))
|
||||
{
|
||||
/*
|
||||
* Set up to track total elapsed time in ExecutorRun. Make sure the
|
||||
|
@ -552,24 +556,24 @@ static void
|
|||
pgss_ExecutorRun(QueryDesc *queryDesc, ScanDirection direction, uint64 count,
|
||||
bool execute_once)
|
||||
{
|
||||
if (nested_level >=0 && nested_level < max_stack_depth)
|
||||
nested_queryids[nested_level] = queryDesc->plannedstmt->queryId;
|
||||
nested_level++;
|
||||
if (exec_nested_level >=0 && exec_nested_level < max_stack_depth)
|
||||
nested_queryids[exec_nested_level] = queryDesc->plannedstmt->queryId;
|
||||
exec_nested_level++;
|
||||
PG_TRY();
|
||||
{
|
||||
if (prev_ExecutorRun)
|
||||
prev_ExecutorRun(queryDesc, direction, count, execute_once);
|
||||
else
|
||||
standard_ExecutorRun(queryDesc, direction, count, execute_once);
|
||||
nested_level--;
|
||||
if (nested_level >=0 && nested_level < max_stack_depth)
|
||||
nested_queryids[nested_level] = UINT64CONST(0);
|
||||
exec_nested_level--;
|
||||
if (exec_nested_level >=0 && exec_nested_level < max_stack_depth)
|
||||
nested_queryids[exec_nested_level] = UINT64CONST(0);
|
||||
}
|
||||
PG_CATCH();
|
||||
{
|
||||
nested_level--;
|
||||
if (nested_level >=0 && nested_level < max_stack_depth)
|
||||
nested_queryids[nested_level] = UINT64CONST(0);
|
||||
exec_nested_level--;
|
||||
if (exec_nested_level >=0 && exec_nested_level < max_stack_depth)
|
||||
nested_queryids[exec_nested_level] = UINT64CONST(0);
|
||||
PG_RE_THROW();
|
||||
}
|
||||
PG_END_TRY();
|
||||
|
@ -592,18 +596,18 @@ pgss_ExecutorFinish_benchmark(QueryDesc *queryDesc)
|
|||
static void
|
||||
pgss_ExecutorFinish(QueryDesc *queryDesc)
|
||||
{
|
||||
nested_level++;
|
||||
exec_nested_level++;
|
||||
PG_TRY();
|
||||
{
|
||||
if (prev_ExecutorFinish)
|
||||
prev_ExecutorFinish(queryDesc);
|
||||
else
|
||||
standard_ExecutorFinish(queryDesc);
|
||||
nested_level--;
|
||||
exec_nested_level--;
|
||||
}
|
||||
PG_CATCH();
|
||||
{
|
||||
nested_level--;
|
||||
exec_nested_level--;
|
||||
PG_RE_THROW();
|
||||
}
|
||||
PG_END_TRY();
|
||||
|
@ -661,7 +665,7 @@ pgss_ExecutorEnd(QueryDesc *queryDesc)
|
|||
MemoryContextSwitchTo(mct);
|
||||
}
|
||||
|
||||
if (queryId != UINT64CONST(0) && queryDesc->totaltime && !IsParallelWorker())
|
||||
if (queryId != UINT64CONST(0) && queryDesc->totaltime && pgsm_enabled(exec_nested_level))
|
||||
{
|
||||
/*
|
||||
* Make sure stats accumulation is done. (Note: it's okay if several
|
||||
|
@ -778,7 +782,20 @@ pgss_planner_hook(Query *parse, const char *query_string, int cursorOptions, Par
|
|||
{
|
||||
PlannedStmt *result;
|
||||
|
||||
if (PGSM_TRACK_PLANNING && query_string && parse->queryId != UINT64CONST(0) && !IsParallelWorker())
|
||||
/*
|
||||
* We can't process the query if no query_string is provided, as
|
||||
* pgss_store needs it. We also ignore query without queryid, as it would
|
||||
* be treated as a utility statement, which may not be the case.
|
||||
*
|
||||
* Note that planner_hook can be called from the planner itself, so we
|
||||
* have a specific nesting level for the planner. However, utility
|
||||
* commands containing optimizable statements can also call the planner,
|
||||
* same for regular DML (for instance for underlying foreign key queries).
|
||||
* So testing the planner nesting level only is not enough to detect real
|
||||
* top level planner call.
|
||||
*/
|
||||
if (pgsm_enabled(plan_nested_level + exec_nested_level) &&
|
||||
PGSM_TRACK_PLANNING && query_string && parse->queryId != UINT64CONST(0))
|
||||
{
|
||||
instr_time start;
|
||||
instr_time duration;
|
||||
|
@ -946,7 +963,7 @@ static void pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
|
|||
* since we are already measuring the statement's costs at the utility
|
||||
* level.
|
||||
*/
|
||||
if (PGSM_TRACK_UTILITY && !IsParallelWorker())
|
||||
if (PGSM_TRACK_UTILITY && pgsm_enabled(exec_nested_level))
|
||||
pstmt->queryId = UINT64CONST(0);
|
||||
#endif
|
||||
|
||||
|
@ -964,8 +981,8 @@ static void pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
|
|||
*
|
||||
* Likewise, we don't track execution of DEALLOCATE.
|
||||
*/
|
||||
if (PGSM_TRACK_UTILITY && PGSM_HANDLED_UTILITY(parsetree) &&
|
||||
!IsParallelWorker())
|
||||
if (PGSM_TRACK_UTILITY && pgsm_enabled(exec_nested_level) &&
|
||||
PGSM_HANDLED_UTILITY(parsetree))
|
||||
{
|
||||
instr_time start;
|
||||
instr_time duration;
|
||||
|
@ -1018,7 +1035,7 @@ static void pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,
|
|||
}
|
||||
PG_CATCH();
|
||||
{
|
||||
nested_level--;
|
||||
exec_nested_level--;
|
||||
PG_RE_THROW();
|
||||
|
||||
}
|
||||
|
@ -1310,10 +1327,10 @@ pgss_update_entry(pgssEntry *entry,
|
|||
|
||||
e->counters.info.cmd_type = cmd_type;
|
||||
|
||||
if(nested_level > 0)
|
||||
if(exec_nested_level > 0)
|
||||
{
|
||||
if (nested_level >=0 && nested_level < max_stack_depth)
|
||||
e->counters.info.parentid = nested_queryids[nested_level - 1];
|
||||
if (exec_nested_level >=0 && exec_nested_level < max_stack_depth)
|
||||
e->counters.info.parentid = nested_queryids[exec_nested_level - 1];
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1429,10 +1446,6 @@ pgss_store(uint64 queryid,
|
|||
static bool found_client_addr = false;
|
||||
static uint client_addr = 0;
|
||||
|
||||
/* Monitoring is disabled */
|
||||
if (!PGSM_ENABLED)
|
||||
return;
|
||||
|
||||
/* Safety check... */
|
||||
if (!IsSystemInitialized())
|
||||
return;
|
||||
|
@ -3315,7 +3328,7 @@ pg_stat_monitor_settings(PG_FUNCTION_ARGS)
|
|||
return (Datum) 0;
|
||||
}
|
||||
|
||||
if (tupdesc->natts != 7)
|
||||
if (tupdesc->natts != 8)
|
||||
{
|
||||
pgsm_log_error("pg_stat_monitor_settings: incorrect number of output arguments, required: 7, found %d", tupdesc->natts);
|
||||
return (Datum) 0;
|
||||
|
@ -3330,19 +3343,78 @@ pg_stat_monitor_settings(PG_FUNCTION_ARGS)
|
|||
|
||||
for(i = 0; i < MAX_SETTINGS; i++)
|
||||
{
|
||||
Datum values[7];
|
||||
bool nulls[7];
|
||||
Datum values[8];
|
||||
bool nulls[8];
|
||||
int j = 0;
|
||||
char options[1024] = "";
|
||||
GucVariable *conf;
|
||||
|
||||
memset(values, 0, sizeof(values));
|
||||
memset(nulls, 0, sizeof(nulls));
|
||||
|
||||
values[j++] = CStringGetTextDatum(get_conf(i)->guc_name);
|
||||
values[j++] = Int64GetDatumFast(get_conf(i)->guc_variable);
|
||||
values[j++] = Int64GetDatumFast(get_conf(i)->guc_default);
|
||||
conf = get_conf(i);
|
||||
|
||||
values[j++] = CStringGetTextDatum(conf->guc_name);
|
||||
|
||||
/* Handle current and default values. */
|
||||
switch (conf->type)
|
||||
{
|
||||
case PGC_ENUM:
|
||||
values[j++] = CStringGetTextDatum(conf->guc_options[conf->guc_variable]);
|
||||
values[j++] = CStringGetTextDatum(conf->guc_options[conf->guc_default]);
|
||||
break;
|
||||
|
||||
case PGC_INT:
|
||||
{
|
||||
char value[32];
|
||||
|
||||
sprintf(value, "%d", conf->guc_variable);
|
||||
values[j++] = CStringGetTextDatum(value);
|
||||
|
||||
sprintf(value, "%d", conf->guc_default);
|
||||
values[j++] = CStringGetTextDatum(value);
|
||||
break;
|
||||
}
|
||||
|
||||
case PGC_BOOL:
|
||||
values[j++] = CStringGetTextDatum(conf->guc_variable ? "yes" : "no");
|
||||
values[j++] = CStringGetTextDatum(conf->guc_default ? "yes" : "no");
|
||||
break;
|
||||
|
||||
default:
|
||||
Assert(false);
|
||||
}
|
||||
|
||||
values[j++] = CStringGetTextDatum(get_conf(i)->guc_desc);
|
||||
values[j++] = Int64GetDatumFast(get_conf(i)->guc_min);
|
||||
values[j++] = Int64GetDatumFast(get_conf(i)->guc_max);
|
||||
values[j++] = Int64GetDatumFast(get_conf(i)->guc_restart);
|
||||
|
||||
/* Minimum and maximum displayed only for integers or real numbers. */
|
||||
if (conf->type != PGC_INT)
|
||||
{
|
||||
nulls[j++] = true;
|
||||
nulls[j++] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
values[j++] = Int64GetDatumFast(get_conf(i)->guc_min);
|
||||
values[j++] = Int64GetDatumFast(get_conf(i)->guc_max);
|
||||
}
|
||||
|
||||
if (conf->type == PGC_ENUM)
|
||||
{
|
||||
strcat(options, conf->guc_options[0]);
|
||||
for (size_t i = 1; i < conf->n_options; ++i)
|
||||
{
|
||||
strcat(options, ", ");
|
||||
strcat(options, conf->guc_options[i]);
|
||||
}
|
||||
}
|
||||
else if (conf->type == PGC_BOOL)
|
||||
{
|
||||
strcat(options, "yes, no");
|
||||
}
|
||||
|
||||
values[j++] = CStringGetTextDatum(options);
|
||||
values[j++] = CStringGetTextDatum(get_conf(i)->guc_restart ? "yes" : "no");
|
||||
tuplestore_putvalues(tupstore, tupdesc, values, nulls);
|
||||
}
|
||||
/* clean up and return the tuplestore */
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
#include "utils/timestamp.h"
|
||||
#include "utils/lsyscache.h"
|
||||
#include "utils/guc.h"
|
||||
#include "utils/guc_tables.h"
|
||||
|
||||
#define MAX_BACKEND_PROCESES (MaxBackends + NUM_AUXILIARY_PROCS + max_prepared_xacts)
|
||||
#define IntArrayGetTextDatum(x,y) intarray_get_datum(x,y)
|
||||
|
@ -99,8 +100,11 @@
|
|||
#define MAX_SETTINGS 13
|
||||
#endif
|
||||
|
||||
/* Update this if need a enum GUC with more options. */
|
||||
#define MAX_ENUM_OPTIONS 6
|
||||
typedef struct GucVariables
|
||||
{
|
||||
enum config_type type; /* PGC_BOOL, PGC_INT, PGC_REAL, PGC_STRING, PGC_ENUM */
|
||||
int guc_variable;
|
||||
char guc_name[TEXT_LEN];
|
||||
char guc_desc[TEXT_LEN];
|
||||
|
@ -110,6 +114,8 @@ typedef struct GucVariables
|
|||
int guc_unit;
|
||||
int *guc_value;
|
||||
bool guc_restart;
|
||||
int n_options;
|
||||
char guc_options[MAX_ENUM_OPTIONS][32];
|
||||
} GucVariable;
|
||||
|
||||
#if PG_VERSION_NUM < 130000
|
||||
|
@ -408,22 +414,38 @@ void set_qbuf(unsigned char *);
|
|||
|
||||
/* hash_query.c */
|
||||
void pgss_startup(void);
|
||||
|
||||
/*---- GUC variables ----*/
|
||||
typedef enum {
|
||||
PSGM_TRACK_NONE, /* track no statements */
|
||||
PGSM_TRACK_TOP, /* only top level statements */
|
||||
PGSM_TRACK_ALL /* all statements, including nested ones */
|
||||
} PGSMTrackLevel;
|
||||
|
||||
static const struct config_enum_entry track_options[] =
|
||||
{
|
||||
{"none", PSGM_TRACK_NONE, false},
|
||||
{"top", PGSM_TRACK_TOP, false},
|
||||
{"all", PGSM_TRACK_ALL, false},
|
||||
{NULL, 0, false}
|
||||
};
|
||||
|
||||
#define PGSM_MAX get_conf(0)->guc_variable
|
||||
#define PGSM_QUERY_MAX_LEN get_conf(1)->guc_variable
|
||||
#define PGSM_ENABLED get_conf(2)->guc_variable
|
||||
#define PGSM_TRACK_UTILITY get_conf(3)->guc_variable
|
||||
#define PGSM_NORMALIZED_QUERY get_conf(4)->guc_variable
|
||||
#define PGSM_MAX_BUCKETS get_conf(5)->guc_variable
|
||||
#define PGSM_BUCKET_TIME get_conf(6)->guc_variable
|
||||
#define PGSM_HISTOGRAM_MIN get_conf(7)->guc_variable
|
||||
#define PGSM_HISTOGRAM_MAX get_conf(8)->guc_variable
|
||||
#define PGSM_HISTOGRAM_BUCKETS get_conf(9)->guc_variable
|
||||
#define PGSM_QUERY_SHARED_BUFFER get_conf(10)->guc_variable
|
||||
#define PGSM_OVERFLOW_TARGET get_conf(11)->guc_variable
|
||||
#define PGSM_QUERY_PLAN get_conf(12)->guc_variable
|
||||
#define PGSM_TRACK_UTILITY get_conf(2)->guc_variable
|
||||
#define PGSM_NORMALIZED_QUERY get_conf(3)->guc_variable
|
||||
#define PGSM_MAX_BUCKETS get_conf(4)->guc_variable
|
||||
#define PGSM_BUCKET_TIME get_conf(5)->guc_variable
|
||||
#define PGSM_HISTOGRAM_MIN get_conf(6)->guc_variable
|
||||
#define PGSM_HISTOGRAM_MAX get_conf(7)->guc_variable
|
||||
#define PGSM_HISTOGRAM_BUCKETS get_conf(8)->guc_variable
|
||||
#define PGSM_QUERY_SHARED_BUFFER get_conf(9)->guc_variable
|
||||
#define PGSM_OVERFLOW_TARGET get_conf(10)->guc_variable
|
||||
#define PGSM_QUERY_PLAN get_conf(11)->guc_variable
|
||||
#define PGSM_TRACK get_conf(12)->guc_variable
|
||||
#define PGSM_TRACK_PLANNING get_conf(13)->guc_variable
|
||||
|
||||
|
||||
/*---- Benchmarking ----*/
|
||||
#ifdef BENCHMARK
|
||||
/*
|
||||
|
|
Loading…
Reference in New Issue