Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,7 @@
"concepts": "cpp",
"algorithm": "cpp"
},
"java.configuration.updateBuildConfiguration": "automatic"
"java.configuration.updateBuildConfiguration": "automatic",
"makefile.configureOnOpen": false,
"java.compile.nullAnalysis.mode": "disabled"
}
106 changes: 53 additions & 53 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,53 +1,53 @@
BUILDLIB?=MANZAN
BUILDVERSION:="Development build \(built with Make\)"
.PHONY: ile camel test
ile:
gmake -C ile BUILDLIB=${BUILDLIB}
camel:
gmake -C camel
test: install
gmake -C test/e2e runtests
testonly:
gmake -C test/e2e runtests
all: ile camel
install:
gmake -C config install BUILDLIB=${BUILDLIB}
gmake -C ile BUILDLIB=${BUILDLIB}
gmake -C camel install
install -m 600 -o qsys service-commander-def.yaml ${INSTALL_ROOT}/opt/manzan/lib/manzan.yaml
uninstall:
gmake -C ile uninstall BUILDLIB=${BUILDLIB}
gmake -C config uninstall BUILDLIB=${BUILDLIB}
/QOpenSys/pkgs/bin/zip:
/QOpenSys/pkgs/bin/yum install zip
/QOpenSys/pkgs/bin/wget:
/QOpenSys/pkgs/bin/yum install wget
appinstall.jar: /QOpenSys/pkgs/bin/wget
/QOpenSys/pkgs/bin/wget -O appinstall.jar https://github.com/ThePrez/AppInstall-IBMi/releases/download/v0.0.6/appinstall-v0.0.6.jar
manzan-installer-v%.jar: /QOpenSys/pkgs/bin/zip appinstall.jar
echo "Building version $*"
system "dltlib ${BUILDLIB}" || echo "could not delete"
system "crtlib ${BUILDLIB}"
system "dltlib ${BUILDLIB}"
> config/app.ini > config/data.ini > config/dests.ini
rm -fr /QOpenSys/etc/manzan
rm -fr /opt/manzan
gmake -C config BUILDVERSION="$*" install BUILDLIB=${BUILDLIB}
gmake -C ile BUILDVERSION="$*" BUILDLIB=${BUILDLIB}
gmake -C camel BUILDVERSION="$*" clean install
install -m 600 -o qsys service-commander-def.yaml ${INSTALL_ROOT}/opt/manzan/lib/manzan.yaml
echo "#!/QOpenSys/usr/bin/sh" > .postinstall
echo "ln -sf ${INSTALL_ROOT}/opt/manzan/lib/manzan.yaml /QOpenSys/etc/sc/services/manzan.yaml" >> .postinstall
/QOpenSys/QIBM/ProdData/JavaVM/jdk80/64bit/bin/java -jar appinstall.jar -o $@ --qsys manzan --fileIfMissing /QOpenSys/etc/manzan --file /opt/manzan --post .postinstall
BUILDLIB?=MANZAN
BUILDVERSION:="Development build \(built with Make\)"

.PHONY: ile camel test

ile:
gmake -C ile BUILDLIB=${BUILDLIB}

camel:
gmake -C camel

test: install
gmake -C test/e2e runtests

testonly:
gmake -C test/e2e runtests

all: ile camel

install:
gmake -C config install BUILDLIB=${BUILDLIB}
gmake -C ile BUILDLIB=${BUILDLIB}
gmake -C camel install
install -m 600 -o qsys service-commander-def.yaml ${INSTALL_ROOT}/opt/manzan/lib/manzan.yaml

uninstall:
gmake -C ile uninstall BUILDLIB=${BUILDLIB}
gmake -C config uninstall BUILDLIB=${BUILDLIB}

/QOpenSys/pkgs/bin/zip:
/QOpenSys/pkgs/bin/yum install zip

/QOpenSys/pkgs/bin/wget:
/QOpenSys/pkgs/bin/yum install wget

appinstall.jar: /QOpenSys/pkgs/bin/wget
/QOpenSys/pkgs/bin/wget -O appinstall.jar https://github.com/ThePrez/AppInstall-IBMi/releases/download/v0.0.6/appinstall-v0.0.6.jar

manzan-installer-v%.jar: /QOpenSys/pkgs/bin/zip appinstall.jar
echo "Building version $*"
system "dltlib ${BUILDLIB}" || echo "could not delete"
system "crtlib ${BUILDLIB}"
system "dltlib ${BUILDLIB}"
> config/app.ini > config/data.ini > config/dests.ini
rm -fr /QOpenSys/etc/manzan
rm -fr /opt/manzan
gmake -C config BUILDVERSION="$*" install BUILDLIB=${BUILDLIB}
gmake -C ile BUILDVERSION="$*" BUILDLIB=${BUILDLIB}
gmake -C camel BUILDVERSION="$*" clean install
install -m 600 -o qsys service-commander-def.yaml ${INSTALL_ROOT}/opt/manzan/lib/manzan.yaml
echo "#!/QOpenSys/usr/bin/sh" > .postinstall
echo "ln -sf ${INSTALL_ROOT}/opt/manzan/lib/manzan.yaml /QOpenSys/etc/sc/services/manzan.yaml" >> .postinstall
/QOpenSys/QIBM/ProdData/JavaVM/jdk80/64bit/bin/java -jar appinstall.jar -o $@ --qsys manzan --fileIfMissing /QOpenSys/etc/manzan --file /opt/manzan --post .postinstall
52 changes: 26 additions & 26 deletions camel/Makefile
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
BUILDVERSION:="Development build \(built with Make\)"
.PHONY: mkdirs
JAVA_SRCS := $(shell find src -type f)
target/manzan.jar: ${JAVA_SRCS} /QOpenSys/pkgs/bin/mvn
JAVA_HOME=/QOpenSys/QIBM/ProdData/JavaVM/jdk80/64bit /QOpenSys/pkgs/bin/mvn -Djava.net.preferIPv4Stack=true -Dmanzan.version=${BUILDVERSION} package
cp target/manzan-*-with-dependencies.jar target/manzan.jar
mkdirs:
gmake -C ../config mkdirs
/QOpenSys/pkgs/bin/mvn:
/QOpenSys/pkgs/bin/yum install maven
install: mkdirs all scripts/manzan
install -m 700 -o qsys target/manzan.jar ${INSTALL_ROOT}/opt/manzan/lib/manzan.jar
install -m 700 -o qsys scripts/manzan ${INSTALL_ROOT}/opt/manzan/bin/manzan
${INSTALL_ROOT}/opt/manzan/bin/manzan:
clean:
rm -fr target


BUILDVERSION:="Development build \(built with Make\)"

.PHONY: mkdirs

JAVA_SRCS := $(shell find src -type f)
target/manzan.jar: ${JAVA_SRCS} /QOpenSys/pkgs/bin/mvn
JAVA_HOME=/QOpenSys/QIBM/ProdData/JavaVM/jdk80/64bit /QOpenSys/pkgs/bin/mvn -Djava.net.preferIPv4Stack=true -Dmanzan.version=${BUILDVERSION} package
cp target/manzan-*-with-dependencies.jar target/manzan.jar

mkdirs:
gmake -C ../config mkdirs

/QOpenSys/pkgs/bin/mvn:
/QOpenSys/pkgs/bin/yum install maven

install: mkdirs all scripts/manzan
install -m 700 -o qsys target/manzan.jar ${INSTALL_ROOT}/opt/manzan/lib/manzan.jar
install -m 700 -o qsys scripts/manzan ${INSTALL_ROOT}/opt/manzan/bin/manzan

${INSTALL_ROOT}/opt/manzan/bin/manzan:

clean:
rm -fr target

all: target/manzan.jar
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.github.theprez.manzan;

public enum ManzanEventType {
FILE, WATCH_MSG, WATCH_PAL, WATCH_VLOG, HTTP, AUDIT, CMD, SQL, TABLE
FILE, WATCH_MSG, WATCH_PAL, WATCH_VLOG, HTTP, AUDIT, CMD, SQL, TABLE, JOBLOG
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,20 @@ public synchronized Map<String, ManzanRoute> getRoutes() throws IOException, AS4
Map<String, String> headerParams = getUriAndHeaderParameters(name, sectionObj, "url");
ret.put(name, new HttpEvent(name, url, format, destinations,filter, interval, headerParams, dataMapInjections));
break;
case "joblog":
final String jobs = getRequiredString(name, "jobs");
final List<String> jobIdentifiers = new LinkedList<>();
for (String jobId : jobs.split("\\s*,\\s*")) {
jobId = jobId.trim();
if (StringUtils.isNonEmpty(jobId)) {
jobIdentifiers.add(jobId);
}
}
if (jobIdentifiers.isEmpty()) {
throw new RuntimeException("No valid job identifiers specified for joblog data source '" + name + "'");
}
ret.put(name, new WatchJobLog(name, jobIdentifiers, format, destinations, interval, dataMapInjections));
break;
default:
throw new RuntimeException("Unknown destination type: " + type);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package com.github.theprez.manzan.routes.event;

import java.io.IOException;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.*;
import java.util.regex.Pattern;

import com.github.theprez.jcmdutils.StringUtils;
import com.github.theprez.manzan.ManzanEventType;
import com.github.theprez.manzan.ManzanMessageFormatter;
import com.github.theprez.manzan.routes.ManzanRoute;

public class WatchJobLog extends ManzanRoute {
// Pattern to validate job identifier format: number/user/name
// Job number: 6 digits, User: up to 10 alphanumeric, Name: up to 10 alphanumeric
private static final Pattern JOB_IDENTIFIER_PATTERN = Pattern.compile("^\\d{6}/[A-Z0-9]{1,10}/[A-Z0-9]{1,10}$");

private final int m_interval;
private final ManzanMessageFormatter m_formatter;
private final List<String> m_jobIdentifiers;
private final Map<String, Timestamp> m_lastCheckTimestamps;
private final Map<String, String> dataMapInjection;

public WatchJobLog(final String _name, final List<String> _jobIdentifiers, final String _format,
final List<String> _destinations, final int _interval,
final Map<String, String> _dataMapInjection) throws IOException {
super(_name);
m_interval = _interval;
m_formatter = StringUtils.isEmpty(_format) ? null : new ManzanMessageFormatter(_format);

// Validate all job identifiers before storing them
validateJobIdentifiers(_jobIdentifiers);
m_jobIdentifiers = _jobIdentifiers;
m_lastCheckTimestamps = new HashMap<>();
dataMapInjection = _dataMapInjection;

// Initialize timestamps for each job to current time minus 1 minute
Timestamp initialTimestamp = Timestamp.from(Instant.now().minusSeconds(60));
for (String jobId : m_jobIdentifiers) {
m_lastCheckTimestamps.put(jobId, initialTimestamp);
}

super.setRecipientList(_destinations);
setEventType(ManzanEventType.JOBLOG);
}

protected void setEventType(ManzanEventType eventType) {
m_eventType = eventType;
}

/**
* Validates job identifiers to prevent SQL injection
* Job identifiers must match the format: number/user/name
* @param jobIdentifiers List of job identifiers to validate
* @throws IllegalArgumentException if any job identifier is invalid
*/
private void validateJobIdentifiers(List<String> jobIdentifiers) {
for (String jobId : jobIdentifiers) {
if (jobId == null || !JOB_IDENTIFIER_PATTERN.matcher(jobId.trim().toUpperCase()).matches()) {
throw new IllegalArgumentException(
"Invalid job identifier format: '" + jobId + "'. " +
"Expected format: NNNNNN/USER/JOBNAME (e.g., 123456/QUSER/MYJOB)"
);
}
}
}

/**
* Build SQL query to fetch job log entries for all monitored jobs
* Uses QSYS2.JOBLOG_INFO table function
*/
private String buildJobLogQuery() {
StringBuilder sql = new StringBuilder();

for (int i = 0; i < m_jobIdentifiers.size(); i++) {
// Job identifiers are pre-validated in constructor, safe to use
String jobId = m_jobIdentifiers.get(i);
Timestamp lastCheck = m_lastCheckTimestamps.get(jobId);

if (i > 0) {
sql.append(" UNION ALL ");
}

sql.append("SELECT ");
sql.append("'").append(jobId).append("' AS JOB_FULL, ");
sql.append("MESSAGE_ID, ");
sql.append("MESSAGE_TYPE, ");
sql.append("SEVERITY, ");
sql.append("MESSAGE_TIMESTAMP, ");
sql.append("MESSAGE_TEXT, ");
sql.append("MESSAGE_SECOND_LEVEL_TEXT, ");
sql.append("FROM_PROGRAM, ");
sql.append("FROM_LIBRARY, ");
sql.append("FROM_MODULE, ");
sql.append("FROM_PROCEDURE, ");
sql.append("MESSAGE_KEY ");
sql.append("FROM TABLE(QSYS2.JOBLOG_INFO('").append(jobId).append("')) ");
sql.append("WHERE MESSAGE_TIMESTAMP > '").append(lastCheck.toString()).append("' ");
}

if (m_jobIdentifiers.size() > 0) {
sql.append(" ORDER BY MESSAGE_TIMESTAMP");
}

return sql.toString();
}

/**
* Update the last check timestamp for a job based on the latest message timestamp
*/
private void updateLastCheckTimestamp(String jobId, Timestamp messageTimestamp) {
Timestamp current = m_lastCheckTimestamps.get(jobId);
if (current == null || messageTimestamp.after(current)) {
m_lastCheckTimestamps.put(jobId, messageTimestamp);
}
}

@Override
public void configure() {
from("timer://foo?synchronous=true&period=" + m_interval)
.routeId("manzan_joblog:" + m_name)
.process(exchange -> {
String sql = buildJobLogQuery();
exchange.getIn().setBody(sql);
})
.to("jdbc:jt400?outputType=StreamList")
.split(body()).streaming().parallelProcessing()
.process(exchange -> {
Map<String, Object> dataMap = exchange.getIn().getBody(Map.class);

// Update last check timestamp for this job
String jobId = (String) dataMap.get("JOB_FULL");
Object timestampObj = dataMap.get("MESSAGE_TIMESTAMP");
if (timestampObj instanceof Timestamp) {
updateLastCheckTimestamp(jobId, (Timestamp) timestampObj);
}

// Parse job identifier into components
String[] jobParts = jobId.split("/");
if (jobParts.length == 3) {
dataMap.put("JOB_NUMBER", jobParts[0]);
dataMap.put("JOB_USER", jobParts[1]);
dataMap.put("JOB_NAME", jobParts[2]);
}

// Inject custom data
injectIntoDataMap(dataMap, dataMapInjection);
exchange.getIn().setHeader("data_map", dataMap);
exchange.getIn().setBody(dataMap);
})
.setHeader(EVENT_TYPE, constant(m_eventType))
.marshal().json(true) // TODO: skip this if we are applying a format
.setBody(simple("${body}\n"))
.process(exchange -> {
if (null != m_formatter) {
exchange.getIn().setBody(m_formatter.format(getDataMap(exchange)));
}
})
.recipientList(constant(getRecipientList()))
.parallelProcessing()
.stopOnException()
.end()
.end();
}
}

Loading
Loading