diff options
Diffstat (limited to 'src/pmdas/mailq')
-rw-r--r-- | src/pmdas/mailq/GNUmakefile | 49 | ||||
-rw-r--r-- | src/pmdas/mailq/Install | 117 | ||||
-rw-r--r-- | src/pmdas/mailq/README | 48 | ||||
-rw-r--r-- | src/pmdas/mailq/Remove | 38 | ||||
-rw-r--r-- | src/pmdas/mailq/help | 52 | ||||
-rw-r--r-- | src/pmdas/mailq/mailq.c | 401 | ||||
-rw-r--r-- | src/pmdas/mailq/pmlogconf.summary | 5 | ||||
-rw-r--r-- | src/pmdas/mailq/pmns | 24 | ||||
-rw-r--r-- | src/pmdas/mailq/root | 10 |
9 files changed, 744 insertions, 0 deletions
diff --git a/src/pmdas/mailq/GNUmakefile b/src/pmdas/mailq/GNUmakefile new file mode 100644 index 0000000..1794f1a --- /dev/null +++ b/src/pmdas/mailq/GNUmakefile @@ -0,0 +1,49 @@ +# +# Copyright (c) 2000-2001,2004 Silicon Graphics, Inc. All Rights Reserved. +# +# 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. +# + +TOPDIR = ../../.. +include $(TOPDIR)/src/include/builddefs + +IAM = mailq +DOMAIN = MAILQ + +CMDTARGET = pmdamailq$(EXECSUFFIX) +CFILES = mailq.c +DFILES = README +LSRCFILES = Install Remove root help pmns $(DFILES) pmlogconf.summary +LLDLIBS = $(PCP_PMDALIB) $(LIB_FOR_REGEX) + +PMDADIR = $(PCP_PMDAS_DIR)/$(IAM) +LDIRT = domain.h *.log *.dir *.pag so_locations + +default: $(CMDTARGET) + +include $(BUILDRULES) + +install: default + $(INSTALL) -m 755 -d $(PMDADIR) + $(INSTALL) -m 755 $(CMDTARGET) $(PMDADIR)/$(CMDTARGET) + $(INSTALL) -m 755 Install Remove $(PMDADIR) + $(INSTALL) -m 644 $(DFILES) root help pmns domain.h $(PMDADIR) + $(INSTALL) -m 755 -d $(PCP_VAR_DIR)/config/pmlogconf/$(IAM) + $(INSTALL) -m 644 pmlogconf.summary $(PCP_VAR_DIR)/config/pmlogconf/$(IAM)/summary + +mailq.o: domain.h + +domain.h: ../../pmns/stdpmid + $(DOMAIN_MAKERULE) + +default_pcp: default + +install_pcp: install diff --git a/src/pmdas/mailq/Install b/src/pmdas/mailq/Install new file mode 100644 index 0000000..efe1fd5 --- /dev/null +++ b/src/pmdas/mailq/Install @@ -0,0 +1,117 @@ +#! /bin/sh +# +# Copyright (c) 1997-2000,2003 Silicon Graphics, Inc. All Rights Reserved. +# +# 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. +# +# Install the mailq PMDA and/or PMNS +# + +. $PCP_DIR/etc/pcp.env +. $PCP_SHARE_DIR/lib/pmdaproc.sh + +iam=mailq +pmda_interface=2 +forced_restart=false + +# Do it +# +pmdaSetup + +if $do_pmda +then + + dso_opt=false + socket_opt=false + pipe_opt=true + + mqueue="" + chk="" + + if [ -f /etc/sendmail.cf ] + then + chk=`sed -n '/^O *QueueDirectory *= */s///p' /etc/sendmail.cf` + [ -z "$chk" ] && chk=`sed -n '/^O *Q *\//s//\//p' /etc/sendmail.cf` + fi + + if [ ! -z "$chk" -a -d "$chk" ] + then + mqueue="$chk" + else + mqueue=/var/spool/mqueue + fi + + while true + do + $PCP_ECHO_PROG $PCP_ECHO_N 'Mail queue directory ['"$mqueue"'] '"$PCP_ECHO_C" + read ans + [ -z "$ans" ] && break + if [ -d "$ans" ] + then + mqueue="$ans" + break + fi + echo "Error: \"$ans\" is not a directory" + done + + echo + regex="" + $PCP_ECHO_PROG $PCP_ECHO_N 'Mail basename regex ['"$regex"'] '"$PCP_ECHO_C" + read regex + [ -z "$regex" ] || args="$args -r $regex" + + echo + args="$args $mqueue" + + while true + do + echo 'The default delay thresholds for grouping the pending mail items are:' + echo ' 1 hour, 4 hours, 8 hours, 1 day, 3 days and 7 days' + echo + $PCP_ECHO_PROG $PCP_ECHO_N 'Do you wish to use the default delay thresholds [y]? '"$PCP_ECHO_C" + read ans + if [ -z "$ans" -o "$ans" = "y" -o "$ans" = "Y" ] + then + break + else + bucketlist='' + while true + do + $PCP_ECHO_PROG $PCP_ECHO_N 'Threshold? [return if no more] '"$PCP_ECHO_C" + read ans + [ -z "$ans" ] && break + # strip blanks so args in pmcd.conf get passed correctly to + # the mailqpmda binary + # + ans=`echo "$ans" | sed -e 's/ //g'` + if [ -z "$bucketlist" ] + then + bucketlist="$ans" + else + bucketlist="$bucketlist,$ans" + fi + done + if [ ! -z "$bucketlist" ] + then + args="$args -b $bucketlist" + break + fi + echo + echo 'Error: you must specify at least one threshold' + echo + fi + done + echo +fi + +pmdaInstall + +exit 0 diff --git a/src/pmdas/mailq/README b/src/pmdas/mailq/README new file mode 100644 index 0000000..08bbfb6 --- /dev/null +++ b/src/pmdas/mailq/README @@ -0,0 +1,48 @@ +Mailq PMDA +========== + +This PMDA exports information about the sendmail(1) queue. + +Metrics +======= + +The file ./help contains descriptions for all of the metrics exported +by this PMDA. + +Once the PMDA has been installed, the following command will list all +the available metrics and their explanatory "help" text: + + $ pminfo -fT mailq + +Installation +============ + + + # cd $PCP_PMDAS_DIR/mailq + + + Check that there is no clash in the Performance Metrics Domain defined + in ./domain.h and the other PMDAs currently in use (see + $PCP_PMCDCONF_PATH). If there is, edit ./domain.h to choose another + domain number. + + + Then simply use + + # ./Install + + and choose both the "collector" and "monitor" installation + configuration options. + +De-installation +=============== + + + Simply use + + # cd $PCP_PMDAS_DIR/mailq + # ./Remove + +Troubleshooting +=============== + + + After installing or restarting the agent, the PMCD log file + ($PCP_LOG_DIR/pmcd/pmcd.log) and the PMDA log file + ($PCP_LOG_DIR/pmcd/mailq.log) should be checked for any warnings + or errors. diff --git a/src/pmdas/mailq/Remove b/src/pmdas/mailq/Remove new file mode 100644 index 0000000..77da216 --- /dev/null +++ b/src/pmdas/mailq/Remove @@ -0,0 +1,38 @@ +#! /bin/sh +# +# Copyright (c) 1997 Silicon Graphics, Inc. All Rights Reserved. +# +# 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. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# Remove the mailq PMDA +# + +# Get standard environment +. $PCP_DIR/etc/pcp.env + +# Get the common procedures and variable assignments +# +. $PCP_SHARE_DIR/lib/pmdaproc.sh + +# The name of the PMDA +# +iam=mailq + +# Do it +# +pmdaSetup +pmdaRemove + +exit 0 diff --git a/src/pmdas/mailq/help b/src/pmdas/mailq/help new file mode 100644 index 0000000..29a1792 --- /dev/null +++ b/src/pmdas/mailq/help @@ -0,0 +1,52 @@ +# +# Copyright (c) 2000-2004 Silicon Graphics, Inc. All Rights Reserved. +# +# 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. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# mailq PMDA help file in the ASCII format +# +# lines beginning with a # are ignored +# lines beginning @ introduce a new entry of the form +# @ metric_name oneline-text +# help test goes +# here over multiple lines +# ... +# +# the metric_name is decoded against the default PMNS -- as a special case, +# a name of the form NNN.MM (for numeric NNN and MM) is interpreted as an +# instance domain identification, and the text describes the instance domain +# +# blank lines before the @ line are ignored +# + +@ MAILQ.1 Instance domain used to "bin" messages based on how long they have been deferred + + +@ mailq.length Number of messages in the queue +The total number of messages in the mail queue. + +@ mailq.deferred Histogram of pending messages based on how long they have been deferred +Counts of the number of messages in the sendmail queue grouped by +how long the mail delivery has been deferred. + +The groups are based on the how long a message has been in the +queue: + 1-week at least a week + 3-days at least 3 days and less than a week + 1-day at least one day and less than 3 days + 8-hours more than 8 hours and less than one day + 4-hours between 4 and 8 hours + 1-hour between 1 and 4 hours + recent less than an hour diff --git a/src/pmdas/mailq/mailq.c b/src/pmdas/mailq/mailq.c new file mode 100644 index 0000000..8d82fcd --- /dev/null +++ b/src/pmdas/mailq/mailq.c @@ -0,0 +1,401 @@ +/* + * Mailq PMDA + * + * Copyright (c) 2012,2014 Red Hat. + * Copyright (c) 1997-2000,2003 Silicon Graphics, Inc. All Rights Reserved. + * + * 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. + */ + +#include "pmapi.h" +#include "impl.h" +#include "pmda.h" +#include "domain.h" +#ifdef HAVE_REGEX_H +#include <regex.h> +#endif +#include <sys/stat.h> + +/* + * histogram for binning messages based on queue time + */ +typedef struct { + long count; /* number in this bin */ + time_t delay; /* in queue for at least this long (seconds) */ +} histo_t; + +static histo_t *histo; +static int numhisto; +static int queue; + +/* + * list of instances - indexes must match histo[] above + */ +static pmdaInstid *_delay; + +static char *queuedir = "/var/spool/mqueue"; +static char startdir[MAXPATHLEN]; +static char *username; + +static char *regexstring; +static regex_t mq_regex; + +/* + * list of instance domains + */ +static pmdaIndom indomtab[] = { +#define DELAY_INDOM 0 + { DELAY_INDOM, 0, NULL }, +}; + +/* + * all metrics supported in this PMDA - one table entry for each + */ +static pmdaMetric metrictab[] = { +/* length */ + { NULL, + { PMDA_PMID(0,0), PM_TYPE_U32, PM_INDOM_NULL, PM_SEM_INSTANT, + PMDA_PMUNITS(0,0,1,0,0,PM_COUNT_ONE) }, }, +/* deferred */ + { NULL, + { PMDA_PMID(0,1), PM_TYPE_U32, DELAY_INDOM, PM_SEM_INSTANT, + PMDA_PMUNITS(0,0,1,0,0,PM_COUNT_ONE) }, }, +}; + +static int +mailq_histogram(char *option) +{ + struct timeval tv; + char *errmsg; + char *q; + int sts; + + q = strtok(option, ","); + while (q != NULL) { + if ((sts = pmParseInterval((const char *)q, &tv, &errmsg)) < 0) { + pmprintf("%s: bad historgram bins argument:\n%s\n", pmProgname, errmsg); + free(errmsg); + return -EINVAL; + } + numhisto++; + histo = (histo_t *)realloc(histo, numhisto * sizeof(histo[0])); + if (histo == NULL) + __pmNoMem("histo", numhisto * sizeof(histo[0]), PM_FATAL_ERR); + histo[numhisto-1].delay = tv.tv_sec; + q = strtok(NULL, ","); + } + return 0; +} + +static int +compare_delay(const void *a, const void *b) +{ + histo_t *ha = (histo_t *)a; + histo_t *hb = (histo_t *)b; + + return hb->delay - ha->delay; +} + +/* + * callback provided to pmdaFetch + */ +static int +mailq_fetchCallBack(pmdaMetric *mdesc, unsigned int inst, pmAtomValue *atom) +{ + __pmID_int *idp = (__pmID_int *)&(mdesc->m_desc.pmid); + int b; + + if (idp->cluster == 0) { + if (idp->item == 0) { /* mailq.length */ + if (inst == PM_IN_NULL) + atom->ul = queue; + else + return PM_ERR_INST; + } + else if (idp->item == 1) { /* mailq.deferred */ + /* inst is unsigned, so always >= 0 */ + for (b = 0; b < numhisto; b++) { + if (histo[b].delay == inst) break; + } + if (b < numhisto) + atom->ul = histo[b].count; + else + return PM_ERR_INST; + } + else + return PM_ERR_PMID; + } + else + return PM_ERR_PMID; + + return 0; +} + +/* + * wrapper for pmdaFetch which refreshes the metrics + */ +static int +mailq_fetch(int numpmid, pmID pmidlist[], pmResult **resp, pmdaExt *pmda) +{ + static int warn = 0; + int num; + int i; + int b; + struct stat sbuf; + time_t now; + static time_t last_refresh = 0; + time_t waiting; + char *p; + struct dirent **list; + + time(&now); + + /* clip refresh rate to at most once per 30 seconds */ + if (now - last_refresh > 30) { + last_refresh = now; + + queue = 0; + for (b = 0; b < numhisto; b++) + histo[b].count = 0; + + if (chdir(queuedir) < 0) { + if (warn == 0) { + __pmNotifyErr(LOG_ERR, "chdir(\"%s\") failed: %s\n", + queuedir, osstrerror()); + warn = 1; + } + } + else { + if (warn == 1) { + __pmNotifyErr(LOG_INFO, "chdir(\"%s\") success\n", queuedir); + warn = 0; + } + + num = scandir(".", &list, NULL, NULL); + + for (i = 0; i < num; i++) { + p = list[i]->d_name; + /* only file names that match the regular expression */ + if (regexstring && regexec(&mq_regex, list[i]->d_name, 0, NULL, 0)) + continue; + else if (!regexstring && (*p != 'd' || *(p+1) != 'f')) + continue; + if (stat(p, &sbuf) != 0) { + /* + * ENOENT expected sometimes if sendmail is doing its job + */ + if (oserror() == ENOENT) + continue; + fprintf(stderr, "stat(\"%s\"): %s\n", p, osstrerror()); + continue; + } + if (sbuf.st_size > 0 && S_ISREG(sbuf.st_mode)) { + /* really in the queue */ +#if defined(HAVE_ST_MTIME_WITH_E) + waiting = now - sbuf.st_mtime; +#elif defined(HAVE_ST_MTIME_WITH_SPEC) + waiting = now - sbuf.st_mtimespec.tv_sec; +#else + waiting = now - sbuf.st_mtim.tv_sec; +#endif + for (b = 0; b < numhisto; b++) { + if (waiting >= histo[b].delay) { + histo[b].count++; + break; + } + } + queue++; + } + } + for (i = 0; i < num; i++) + free(list[i]); + if (num > 0) + free(list); + } + if (chdir(startdir) < 0) { + __pmNotifyErr(LOG_ERR, "chdir(\"%s\") failed: %s\n", + startdir, osstrerror()); + } + } + + return pmdaFetch(numpmid, pmidlist, resp, pmda); +} + +/* + * Initialise the agent (daemon only). + */ +void +mailq_init(pmdaInterface *dp) +{ + if (dp->status != 0) + return; + + __pmSetProcessIdentity(username); + dp->version.two.fetch = mailq_fetch; + pmdaSetFetchCallBack(dp, mailq_fetchCallBack); + pmdaInit(dp, indomtab, sizeof(indomtab)/sizeof(indomtab[0]), metrictab, + sizeof(metrictab)/sizeof(metrictab[0])); +} + +pmdaInterface dispatch; + +pmLongOptions longopts[] = { + PMDA_OPTIONS_HEADER("Options"), + PMOPT_DEBUG, + { "binlist", 1, 'b', "TIMES", "comma-separated histogram bins times" }, + PMDAOPT_DOMAIN, + PMDAOPT_LOGFILE, + { "regex", 1, 'r', "RE", "regular expression for matching mail file names" }, + PMDAOPT_USERNAME, + PMOPT_HELP, + PMDA_OPTIONS_END +}; + +pmdaOptions opts = { + .short_options = "b:D:d:l:r:U:?", + .long_options = longopts, + .short_usage = "[options] [queuedir]", +}; + +/* + * Set up the agent, running as a daemon. + */ +int +main(int argc, char **argv) +{ + int sep = __pmPathSeparator(); + int c; + int i; + char namebuf[30]; + char mypath[MAXPATHLEN]; + + __pmSetProgname(argv[0]); + __pmGetUsername(&username); + + if (getcwd(startdir, sizeof(startdir)) == NULL) { + fprintf(stderr, "%s: getcwd() failed: %s\n", + pmProgname, pmErrStr(-oserror())); + exit(1); + } + + snprintf(mypath, sizeof(mypath), "%s%c" "mailq" "%c" "help", + pmGetConfig("PCP_PMDAS_DIR"), sep, sep); + pmdaDaemon(&dispatch, PMDA_INTERFACE_2, pmProgname, MAILQ, + "mailq.log", mypath); + + while ((c = pmdaGetOptions(argc, argv, &opts, &dispatch)) != EOF) { + switch (c) { + case 'b': + if (mailq_histogram(opts.optarg) < 0) + opts.errors++; + break; + + case 'r': + regexstring = opts.optarg; + c = regcomp(&mq_regex, regexstring, REG_EXTENDED | REG_NOSUB); + if (c != 0) { + regerror(c, &mq_regex, mypath, sizeof(mypath)); + pmprintf("%s: cannot compile regular expression: %s\n", + pmProgname, mypath); + opts.errors++; + } + break; + } + } + + if (opts.optind == argc - 1) + queuedir = argv[opts.optind]; + else if (opts.optind != argc) + opts.errors++; + + if (opts.errors) { + pmdaUsageMessage(&opts); + exit(1); + } + + if (opts.username) + username = opts.username; + + if (histo == NULL) { + /* default histo bins, if not already done above ... */ + numhisto = 7; + histo = (histo_t *)malloc(numhisto * sizeof(histo[0])); + if (histo == NULL) { + __pmNoMem("histo", numhisto * sizeof(histo[0]), PM_FATAL_ERR); + } + histo[0].delay = 7 * 24 * 3600; + histo[1].delay = 3 * 24 * 3600; + histo[2].delay = 24 * 3600; + histo[3].delay = 8 * 3600; + histo[4].delay = 4 * 3600; + histo[5].delay = 1 * 3600; + histo[6].delay = 0; + } + else { + /* need to add last one and sort on descending time */ + numhisto++; + histo = (histo_t *)realloc(histo, numhisto * sizeof(histo[0])); + if (histo == NULL) { + __pmNoMem("histo", numhisto * sizeof(histo[0]), PM_FATAL_ERR); + } + histo[numhisto-1].delay = 0; + qsort(histo, numhisto, sizeof(histo[0]), compare_delay); + } + + _delay = (pmdaInstid *)malloc(numhisto * sizeof(_delay[0])); + if (_delay == NULL) + __pmNoMem("_delay", numhisto * sizeof(_delay[0]), PM_FATAL_ERR); + + for (i = 0; i < numhisto; i++) { + time_t tmp; + _delay[i].i_inst = histo[i].delay; + histo[i].count = 0; + if (histo[i].delay == 0) + sprintf(namebuf, "recent"); + else if (histo[i].delay < 60) + sprintf(namebuf, "%d-secs", (int)histo[i].delay); + else if (histo[i].delay < 60 * 60) { + tmp = histo[i].delay / 60; + if (tmp <= 1) + sprintf(namebuf, "1-min"); + else + sprintf(namebuf, "%d-mins", (int)tmp); + } + else if (histo[i].delay < 24 * 60 * 60) { + tmp = histo[i].delay / (60 * 60); + if (tmp <= 1) + sprintf(namebuf, "1-hour"); + else + sprintf(namebuf, "%d-hours", (int)tmp); + } + else { + tmp = histo[i].delay / (24 * 60 * 60); + if (tmp <= 1) + sprintf(namebuf, "1-day"); + else + sprintf(namebuf, "%d-days", (int)tmp); + } + _delay[i].i_name = strdup(namebuf); + if (_delay[i].i_name == NULL) { + __pmNoMem("_delay[i].i_name", strlen(namebuf), PM_FATAL_ERR); + } + } + + indomtab[DELAY_INDOM].it_numinst = numhisto; + indomtab[DELAY_INDOM].it_set = _delay; + + pmdaOpenLog(&dispatch); + mailq_init(&dispatch); + pmdaConnect(&dispatch); + pmdaMain(&dispatch); + + exit(0); +} diff --git a/src/pmdas/mailq/pmlogconf.summary b/src/pmdas/mailq/pmlogconf.summary new file mode 100644 index 0000000..a0534a0 --- /dev/null +++ b/src/pmdas/mailq/pmlogconf.summary @@ -0,0 +1,5 @@ +#pmlogconf-setup 2.0 +ident mailq PMDA summary information +probe mailq.length exists ? include : exclude + mailq.length + mailq.deferred diff --git a/src/pmdas/mailq/pmns b/src/pmdas/mailq/pmns new file mode 100644 index 0000000..3de37aa --- /dev/null +++ b/src/pmdas/mailq/pmns @@ -0,0 +1,24 @@ +/* + * Metrics for mailq PMDA + * + * Copyright (c) 2000-2004 Silicon Graphics, Inc. All Rights Reserved. + * + * 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. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +mailq { + length MAILQ:0:0 + deferred MAILQ:0:1 +} diff --git a/src/pmdas/mailq/root b/src/pmdas/mailq/root new file mode 100644 index 0000000..eab4db5 --- /dev/null +++ b/src/pmdas/mailq/root @@ -0,0 +1,10 @@ +/* + * fake "root" for validating the local PMNS subtree + */ + +#include <stdpmid> + +root { mailq } + +#include "pmns" + |