azkaban-aplcache

Merge pull request #657 from rajagopr/analyze_execution Made

6/3/2016 9:26:24 PM

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);
+  }  
+}
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)