azkaban-aplcache
Details
diff --git a/azkaban-execserver/src/package/conf/azkaban.properties b/azkaban-execserver/src/package/conf/azkaban.properties
index 79e20e1..8a94c35 100644
--- a/azkaban-execserver/src/package/conf/azkaban.properties
+++ b/azkaban-execserver/src/package/conf/azkaban.properties
@@ -25,6 +25,22 @@ executor.flow.threads=30
jetty.connector.stats=true
executor.connector.stats=true
+#
+# External analyzer settings
+# When enabled a button will appear in the flow execution details page which can be accessed
+# to query an external analyzer like Dr. Elephant with the flow execution url.
+# '%url' in 'execution.external.link.url' will be replaced with flow execution url.
+#
+# Note: '%url' is used instead of '%flow_exec_id' as flow execution id is not unique
+# across azkaban instances. The hostname in the url can be relied upon to distinguish
+# between two flows with the same execution id.
+#
+# Set 'execution.external.link.label' to change the button label. It may be configured
+# to reflect the analyzer application.
+#
+# execution.external.link.url=http://elephant.linkedin.com:8080/search?flow-exec-id=%url
+# execution.external.link.label=Dr. Elephant
+
# uncomment to enable inmemory stats for azkaban
#executor.metric.reports=true
#executor.metric.milisecinterval.default=60000
\ No newline at end of file
diff --git a/azkaban-webserver/src/main/java/azkaban/webapp/servlet/ExecutorServlet.java b/azkaban-webserver/src/main/java/azkaban/webapp/servlet/ExecutorServlet.java
index ef588f7..3d36e63 100644
--- a/azkaban-webserver/src/main/java/azkaban/webapp/servlet/ExecutorServlet.java
+++ b/azkaban-webserver/src/main/java/azkaban/webapp/servlet/ExecutorServlet.java
@@ -28,6 +28,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.log4j.Logger;
import azkaban.executor.ConnectorParams;
import azkaban.executor.ExecutableFlow;
@@ -49,17 +50,19 @@ import azkaban.server.HttpRequestUtils;
import azkaban.server.session.Session;
import azkaban.user.Permission;
import azkaban.user.Permission.Type;
-import azkaban.user.Role;
import azkaban.user.User;
import azkaban.user.UserManager;
import azkaban.utils.FileIOUtils.LogData;
import azkaban.utils.Pair;
+import azkaban.utils.Props;
import azkaban.webapp.AzkabanWebServer;
import azkaban.webapp.plugin.PluginRegistry;
import azkaban.webapp.plugin.ViewerPlugin;
public class ExecutorServlet extends LoginAbstractAzkabanServlet {
- private static final long serialVersionUID = 1L;
+ private static final Logger LOGGER =
+ Logger.getLogger(ExecutorServlet.class.getName());
+ private static final long serialVersionUID = 1L;
private ProjectManager projectManager;
private ExecutorManagerAdapter executorManager;
private ScheduleManager scheduleManager;
@@ -338,7 +341,23 @@ public class ExecutorServlet extends LoginAbstractAzkabanServlet {
page.render();
return;
}
-
+
+ Props props = getApplication().getServerProps();
+ String execExternalLinkURL =
+ ExternalAnalyzerUtils.getExternalAnalyzer(props, req);
+
+ if(execExternalLinkURL.length() > 0) {
+ page.add("executionExternalLinkURL", execExternalLinkURL);
+ LOGGER.debug("Added an External analyzer to the page");
+ LOGGER.debug("External analyzer url: " + execExternalLinkURL);
+
+ String execExternalLinkLabel =
+ props.getString(ExternalAnalyzerUtils.EXECUTION_EXTERNAL_LINK_LABEL,
+ "External Analyzer");
+ page.add("executionExternalLinkLabel", execExternalLinkLabel);
+ LOGGER.debug("External analyzer label set to : " + execExternalLinkLabel);
+ }
+
page.add("projectId", project.getId());
page.add("projectName", project.getName());
page.add("flowid", flow.getFlowId());
diff --git a/azkaban-webserver/src/main/java/azkaban/webapp/servlet/ExternalAnalyzerUtils.java b/azkaban-webserver/src/main/java/azkaban/webapp/servlet/ExternalAnalyzerUtils.java
new file mode 100644
index 0000000..0def0ed
--- /dev/null
+++ b/azkaban-webserver/src/main/java/azkaban/webapp/servlet/ExternalAnalyzerUtils.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2016 LinkedIn Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package azkaban.webapp.servlet;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.log4j.Logger;
+
+import azkaban.utils.Props;
+
+
+public final class ExternalAnalyzerUtils {
+ private static final Logger LOGGER =
+ Logger.getLogger(ExternalAnalyzerUtils.class.getName());
+ public static final String EXECUTION_EXTERNAL_LINK_URL =
+ "execution.external.link.url";
+ public static final String EXECUTION_EXTERNAL_LINK_LABEL =
+ "execution.external.link.label";
+
+ private ExternalAnalyzerUtils() {
+
+ }
+
+ /**
+ * Gets an external analyzer URL if configured in 'azkaban.properties'.
+ *
+ * @param props The props to be set to get the external analyzer URL.
+ *
+ * @param req The <code>HttpServletRequest</code> requesting the page.
+ *
+ * @return Returns an external analyzer URL.
+ */
+ public static String getExternalAnalyzer(Props props, HttpServletRequest req) {
+ String url = props.getString(EXECUTION_EXTERNAL_LINK_URL, "");
+ int index = url.indexOf('%');
+
+ if (StringUtils.isNotEmpty(url) && index != -1) {
+ String pattern = url.substring(url.indexOf('%'), url.length());
+
+ switch (pattern) {
+ case "%url":
+ return buildExternalAnalyzerURL(req, url, pattern);
+ default:
+ LOGGER.error("Pattern configured is not supported. "
+ + "Please check the comments section in 'azkaban.properties' "
+ + "for supported patterns.");
+ return "";
+ }
+ }
+ LOGGER.debug("An optional external analyzer is not configured.");
+ return "";
+ }
+
+ private static String
+ buildExternalAnalyzerURL(HttpServletRequest req, String url, String pattern) {
+ StringBuilder builder = new StringBuilder();
+ builder.append(req.getRequestURL());
+ builder.append("?");
+ builder.append(req.getQueryString());
+ String flowExecutionURL = builder.toString();
+ String encodedFlowExecUrl = "";
+ try {
+ encodedFlowExecUrl = URLEncoder.encode(flowExecutionURL, "UTF-8");
+ } catch(UnsupportedEncodingException e) {
+ LOGGER.error("Specified encoding is not supported", e);
+ }
+ return url.replaceFirst(pattern, encodedFlowExecUrl);
+ }
+}
azkaban-webserver/src/main/less/flow.less 14(+14 -0)
diff --git a/azkaban-webserver/src/main/less/flow.less b/azkaban-webserver/src/main/less/flow.less
index 80272bb..01e7386 100644
--- a/azkaban-webserver/src/main/less/flow.less
+++ b/azkaban-webserver/src/main/less/flow.less
@@ -340,3 +340,17 @@ li.tree-list-item {
}
}
}
+
+#analyzerButton {
+ padding: 5px 10px;
+ font-size: 12px;
+ line-height: 1.5;
+ border-radius: 3px;
+
+ &:hover, &:focus {
+ color: #ffffff;
+ text-decoration: none;
+ border-color: #39b3d7;
+ background-color: #269abc;
+ }
+}
diff --git a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/executingflowpage.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/executingflowpage.vm
index 672da57..74c9d6a 100644
--- a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/executingflowpage.vm
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/executingflowpage.vm
@@ -107,6 +107,9 @@
<li id="jobslistViewLink"><a href="#jobslist">Job List</a></li>
<li id="flowLogViewLink"><a href="#log">Flow Log</a></li>
<li id="statsViewLink"><a href="#stats">Stats</a></li>
+ #if ($executionExternalLinkURL)
+ <li><a id="analyzerButton" href="${executionExternalLinkURL}" class="btn btn-info btn-sm" type="button" target="_blank" title="Analyze job in ${executionExternalLinkLabel}">${executionExternalLinkLabel}</a></li>
+ #end
<li class="nav-button pull-right"><button type="button" id="pausebtn" class="btn btn-primary btn-sm">Pause</button></li>
<li class="nav-button pull-right"><button type="button" id="resumebtn" class="btn btn-primary btn-sm">Resume</button></li>
<li class="nav-button pull-right"><button type="button" id="cancelbtn" class="btn btn-danger btn-sm">Kill</button></li>
diff --git a/azkaban-webserver/src/test/java/azkaban/fixture/VelocityTemplateTestUtil.java b/azkaban-webserver/src/test/java/azkaban/fixture/VelocityTemplateTestUtil.java
index fa01234..8eb730f 100644
--- a/azkaban-webserver/src/test/java/azkaban/fixture/VelocityTemplateTestUtil.java
+++ b/azkaban-webserver/src/test/java/azkaban/fixture/VelocityTemplateTestUtil.java
@@ -10,7 +10,7 @@ import org.apache.velocity.app.VelocityEngine;
*/
public class VelocityTemplateTestUtil {
- private static final String TEMPLATE_BASE_DIR = "src/main/resources/azkaban/webapp/servlet/velocity/";
+ private static final String TEMPLATE_BASE_DIR = "azkaban/webapp/servlet/velocity/";
/**
* Render a template and return the result
@@ -22,6 +22,7 @@ public class VelocityTemplateTestUtil {
public static String renderTemplate(String templateName, VelocityContext context) {
StringWriter stringWriter = new StringWriter();
VelocityEngine engine = new VelocityEngine();
+ engine.init("src/test/resources/velocity.properties");
engine.mergeTemplate(TEMPLATE_BASE_DIR + templateName + ".vm", "UTF-8", context, stringWriter);
return stringWriter.getBuffer().toString();
diff --git a/azkaban-webserver/src/test/java/azkaban/webapp/servlet/ExecutionFlowViewTest.java b/azkaban-webserver/src/test/java/azkaban/webapp/servlet/ExecutionFlowViewTest.java
new file mode 100644
index 0000000..bebfa38
--- /dev/null
+++ b/azkaban-webserver/src/test/java/azkaban/webapp/servlet/ExecutionFlowViewTest.java
@@ -0,0 +1,40 @@
+package azkaban.webapp.servlet;
+
+import static org.junit.Assert.assertTrue;
+
+import org.apache.velocity.VelocityContext;
+import org.junit.Test;
+
+import azkaban.fixture.VelocityContextTestUtil;
+import azkaban.fixture.VelocityTemplateTestUtil;
+
+/**
+ * Test flow execution page.
+ */
+public class ExecutionFlowViewTest {
+
+ private static final String EXTERNAL_ANALYZER_ELEMENT =
+ "<li><a id=\"analyzerButton\" href=\"http://elephant.linkedin.com/\" "
+ + "class=\"btn btn-info btn-sm\" type=\"button\" target=\"_blank\" "
+ + "title=\"Analyze job in Dr. Elephant\">Dr. Elephant</a></li>";
+ /**
+ * Test aims to check that the external analyzer button is displayed
+ * in the page.
+ * @throws Exception the exception
+ */
+ @Test
+ public void testExternalAnalyzerButton() throws Exception {
+ VelocityContext context = VelocityContextTestUtil.getInstance();
+
+ context.put("execid", 1);
+ context.put("executionExternalLinkURL", "http://elephant.linkedin.com/");
+ context.put("executionExternalLinkLabel", "Dr. Elephant");
+ context.put("projectId", 1001);
+ context.put("projectName", "user-hello-pig-azkaban");
+ context.put("flowid", 27);
+
+ String result =
+ VelocityTemplateTestUtil.renderTemplate("executingflowpage", context);
+ assertTrue(result.contains(EXTERNAL_ANALYZER_ELEMENT));
+ }
+}
diff --git a/azkaban-webserver/src/test/java/azkaban/webapp/servlet/ExternalAnalyzerUtilsTest.java b/azkaban-webserver/src/test/java/azkaban/webapp/servlet/ExternalAnalyzerUtilsTest.java
new file mode 100644
index 0000000..d50edad
--- /dev/null
+++ b/azkaban-webserver/src/test/java/azkaban/webapp/servlet/ExternalAnalyzerUtilsTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2016 LinkedIn Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package azkaban.webapp.servlet;
+
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import azkaban.utils.Props;
+
+public class ExternalAnalyzerUtilsTest {
+ private Props props;
+ private HttpServletRequest mockRequest;
+ private static final String EXEC_URL = "http://localhost:8081/executor";
+ private static final String EXEC_QUERY_STRING = "execid=1";
+
+ private static final String EXTERNAL_ANALYZER_URL_VALID_FORMAT =
+ "http://elephant.linkedin.com/search?q=%url";
+
+ private static final String EXTERNAL_ANALYZER_URL_WRONG_FORMAT =
+ "http://elephant.linkedin.com/search?q=%unsupported";
+
+ private static final String EXTERNAL_ANALYZER_URL_NO_FORMAT =
+ "http://elephant.linkedin.com/search?q=";
+
+ private static final String EXTERNAL_ANALYZER_EXPECTED_URL =
+ "http://elephant.linkedin.com/search?q="
+ + "http%3A%2F%2Flocalhost%3A8081%2Fexecutor%3Fexecid%3D1";
+
+ @Before
+ public void setUp() {
+ props = new Props();
+ mockRequest = mock(HttpServletRequest.class);
+ }
+
+ /**
+ * Test validates the happy path when an external analyzer is configured
+ * with '%url' as the format in 'azkaban.properties'.
+ */
+ @Test
+ public void testGetExternalAnalyzerValidFormat() {
+ props.put(ExternalAnalyzerUtils.EXECUTION_EXTERNAL_LINK_URL,
+ EXTERNAL_ANALYZER_URL_VALID_FORMAT);
+
+ when(mockRequest.getRequestURL()).thenReturn(new StringBuffer(EXEC_URL));
+ when(mockRequest.getQueryString()).thenReturn(EXEC_QUERY_STRING);
+
+ String executionExternalLinkURL =
+ ExternalAnalyzerUtils.getExternalAnalyzer(props, mockRequest);
+ assertTrue(executionExternalLinkURL.equals(EXTERNAL_ANALYZER_EXPECTED_URL));
+ }
+
+ /**
+ * Test validates the condition when an unsupported pattern is specified
+ * in the url. e.g. '%url1', '%id' etc...
+ */
+ @Test
+ public void testGetExternalAnalyzerWrongFormat() {
+ props.put(ExternalAnalyzerUtils.EXECUTION_EXTERNAL_LINK_URL,
+ EXTERNAL_ANALYZER_URL_WRONG_FORMAT);
+
+ String executionExternalLinkURL =
+ ExternalAnalyzerUtils.getExternalAnalyzer(props, mockRequest);
+ assertTrue(executionExternalLinkURL.equals(""));
+ }
+
+ /**
+ * Test validates the condition when '%url' is not specified.
+ */
+ @Test
+ public void testGetExternalAnalyzerNoFormat() {
+ props.put(ExternalAnalyzerUtils.EXECUTION_EXTERNAL_LINK_URL,
+ EXTERNAL_ANALYZER_URL_NO_FORMAT);
+
+ String executionExternalLinkURL =
+ ExternalAnalyzerUtils.getExternalAnalyzer(props, mockRequest);
+ assertTrue(executionExternalLinkURL.equals(""));
+ }
+
+ /**
+ * Test validates the condition when an external analyzer is not configured
+ * in 'azkaban.properties'.
+ */
+ @Test
+ public void testGetExternalAnalyzerNotConfigured() {
+ String executionExternalLinkURL =
+ ExternalAnalyzerUtils.getExternalAnalyzer(props, mockRequest);
+ assertTrue(executionExternalLinkURL.equals(""));
+ }
+}
diff --git a/azkaban-webserver/src/test/resources/velocity.properties b/azkaban-webserver/src/test/resources/velocity.properties
new file mode 100644
index 0000000..0d16fe7
--- /dev/null
+++ b/azkaban-webserver/src/test/resources/velocity.properties
@@ -0,0 +1,7 @@
+resource.loader = file
+
+file.resource.loader.description = Velocity File Resource Loader
+file.resource.loader.class = org.apache.velocity.runtime.resource.loader.FileResourceLoader
+file.resource.loader.path = src/main/resources/
+file.resource.loader.cache = true
+file.resource.loader.modificationCheckInterval = 10
build.gradle 5(+5 -0)
diff --git a/build.gradle b/build.gradle
index f436fb1..d473a72 100644
--- a/build.gradle
+++ b/build.gradle
@@ -234,6 +234,11 @@ project(':azkaban-webserver') {
compile('org.mortbay.jetty:jetty:6.1.26')
compile('org.mortbay.jetty:jetty-util:6.1.26')
+ testCompile('commons-collections:commons-collections:3.2.2')
+ testCompile('junit:junit:4.11')
+ testCompile('org.hamcrest:hamcrest-all:1.3')
+ testCompile('org.mockito:mockito-all:1.10.19')
+
generateRestli('com.linkedin.pegasus:generator:' + pegasusVersion)
generateRestli('com.linkedin.pegasus:restli-tools:' + pegasusVersion)