You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by al...@apache.org on 2015/07/16 01:09:46 UTC

[02/12] incubator-brooklyn git commit: brooklyn-qa: add org.apache package prefix

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bd44bb8f/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedMySqlNodeImpl.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedMySqlNodeImpl.java b/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedMySqlNodeImpl.java
new file mode 100644
index 0000000..8a24f73
--- /dev/null
+++ b/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedMySqlNodeImpl.java
@@ -0,0 +1,183 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.qa.load;
+
+import static java.lang.String.format;
+
+import java.util.concurrent.Callable;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.AbstractSoftwareProcessSshDriver;
+import brooklyn.entity.database.mysql.MySqlNode;
+import brooklyn.entity.database.mysql.MySqlNodeImpl;
+import brooklyn.entity.database.mysql.MySqlSshDriver;
+import brooklyn.entity.software.SshEffectorTasks;
+import brooklyn.event.feed.function.FunctionFeed;
+import brooklyn.event.feed.function.FunctionPollConfig;
+import brooklyn.location.basic.SshMachineLocation;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.task.DynamicTasks;
+import brooklyn.util.task.system.ProcessTaskWrapper;
+import brooklyn.util.time.CountdownTimer;
+import brooklyn.util.time.Duration;
+
+/**
+ * @see SimulatedJBoss7ServerImpl for description of purpose and configuration options.
+ */
+public class SimulatedMySqlNodeImpl extends MySqlNodeImpl {
+
+    public static final ConfigKey<Boolean> SIMULATE_ENTITY = SimulatedTheeTierApp.SIMULATE_ENTITY;
+    public static final ConfigKey<Boolean> SIMULATE_EXTERNAL_MONITORING = SimulatedTheeTierApp.SIMULATE_EXTERNAL_MONITORING;
+    public static final ConfigKey<Boolean> SKIP_SSH_ON_START = SimulatedTheeTierApp.SKIP_SSH_ON_START;
+    
+    private FunctionFeed feed;
+    
+    @Override
+    public Class<?> getDriverInterface() {
+        return SimulatedMySqlSshDriver.class;
+    }
+
+    @Override
+    protected void connectSensors() {
+        boolean simulateExternalMonitoring = getConfig(SIMULATE_EXTERNAL_MONITORING);
+        if (simulateExternalMonitoring) {
+            setAttribute(DATASTORE_URL, String.format("mysql://%s:%s/", getAttribute(HOSTNAME), getAttribute(MYSQL_PORT)));
+            
+            feed = FunctionFeed.builder()
+                    .entity(this)
+                    .period(Duration.FIVE_SECONDS)
+                    .poll(new FunctionPollConfig<Boolean, Boolean>(SERVICE_UP)
+                            .callable(new Callable<Boolean>() {
+                                private int counter = 0;
+                                public Boolean call() {
+                                    setAttribute(QUERIES_PER_SECOND_FROM_MYSQL, (double)(counter++ % 100));
+                                    return true;
+                                }})
+                            .setOnFailureOrException(false))
+                    .build();
+        } else {
+            super.connectSensors();
+        }
+    }
+
+    public static class SimulatedMySqlSshDriver extends MySqlSshDriver {
+
+        private int counter = 0;
+        
+        public SimulatedMySqlSshDriver(SimulatedMySqlNodeImpl entity, SshMachineLocation machine) {
+            super(entity, machine);
+        }
+        
+        // simulate metrics, for if using ssh polling
+        @Override
+        public String getStatusCmd() {
+            if (entity.getConfig(SIMULATE_ENTITY)) {
+                return "echo Uptime: 2427  Threads: 1  Questions: 581  Slow queries: 0  Opens: 53  Flush tables: 1  Open tables: 35  Queries per second avg: "+(counter++ % 100);
+            } else {
+                return super.getStatusCmd();
+            }
+        }
+
+        @Override
+        public void install() {
+            if (entity.getConfig(SKIP_SSH_ON_START)) {
+                // no-op
+            } else {
+                super.install();
+            }
+        }
+        
+        // Not applying creation-script etc, as that requires launching msyqld (so would not scale for single-machine testing)
+        // This is a copy of super.customize, but with the mysqladmin-exec disabled
+        @Override
+        public void customize() {
+            if (!entity.getConfig(SIMULATE_ENTITY)) {
+                super.customize();
+                return;
+            } else if (entity.getConfig(SKIP_SSH_ON_START)) {
+                // no-op
+            } else {
+                copyDatabaseConfigScript();
+    
+                newScript(CUSTOMIZING)
+                    .updateTaskAndFailOnNonZeroResultCode()
+                    .body.append(
+                        "chmod 600 "+getConfigFile(),
+                        getBaseDir()+"/scripts/mysql_install_db "+
+                            "--basedir="+getBaseDir()+" --datadir="+getDataDir()+" "+
+                            "--defaults-file="+getConfigFile())
+                    .execute();
+    
+                // launch, then we will configure it
+                launch();
+    
+                CountdownTimer timer = Duration.seconds(20).countdownTimer();
+                boolean hasCreationScript = copyDatabaseCreationScript();
+                timer.waitForExpiryUnchecked();
+    
+                // DELIBERATELY SKIPPED FOR SCALABILITY TESTING ON SINGLE MACHINE
+                DynamicTasks.queue(
+                    SshEffectorTasks.ssh(
+                        "cd "+getRunDir(),
+                        "echo skipping exec of "+getBaseDir()+"/bin/mysqladmin --defaults-file="+getConfigFile()+" --password= password "+getPassword()
+                    ).summary("setting password"));
+    
+                if (hasCreationScript)
+                    executeScriptFromInstalledFileAsync("creation-script.sql");
+    
+                // not sure necessary to stop then subsequently launch, but seems safest
+                // (if skipping, use a flag in launch to indicate we've just launched it)
+                stop();
+            }
+        }
+
+        @Override
+        public void launch() {
+            if (!entity.getConfig(SIMULATE_ENTITY)) {
+                super.launch();
+                return;
+            }
+            
+            entity.setAttribute(MySqlNode.PID_FILE, getRunDir() + "/" + AbstractSoftwareProcessSshDriver.PID_FILENAME);
+            
+            if (entity.getConfig(SKIP_SSH_ON_START)) {
+                // minimal ssh, so that isRunning will subsequently work
+                newScript(MutableMap.of("usePidFile", true), LAUNCHING)
+                        .body.append(
+                                format("nohup sleep 100000 > %s 2>&1 < /dev/null &", getLogFile()))
+                        .execute();
+            } else {
+                newScript(MutableMap.of("usePidFile", true), LAUNCHING)
+                    .updateTaskAndFailOnNonZeroResultCode()
+                    .body.append(format("echo skipping normal exec of nohup %s/bin/mysqld --defaults-file=%s --user=`whoami` > %s 2>&1 < /dev/null &", getBaseDir(), getConfigFile(), getLogFile()))
+                    .body.append(format("nohup sleep 100000 > %s 2>&1 < /dev/null &", getLogFile()))
+                    .execute();
+            }
+        }
+
+        @Override
+        public ProcessTaskWrapper<Integer> executeScriptFromInstalledFileAsync(String filenameAlreadyInstalledAtServer) {
+            return DynamicTasks.queue(
+                    SshEffectorTasks.ssh(
+                                    "cd "+getRunDir(),
+                                    "echo skipping exec of "+getBaseDir()+"/bin/mysql --defaults-file="+getConfigFile()+" < "+filenameAlreadyInstalledAtServer)
+                            .summary("executing datastore script "+filenameAlreadyInstalledAtServer));
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bd44bb8f/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedNginxControllerImpl.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedNginxControllerImpl.java b/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedNginxControllerImpl.java
new file mode 100644
index 0000000..8813a4f
--- /dev/null
+++ b/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedNginxControllerImpl.java
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.qa.load;
+
+import static java.lang.String.format;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.concurrent.Callable;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.Group;
+import brooklyn.entity.proxy.nginx.NginxControllerImpl;
+import brooklyn.entity.proxy.nginx.NginxSshDriver;
+import brooklyn.entity.proxy.nginx.UrlMapping;
+import brooklyn.event.SensorEvent;
+import brooklyn.event.SensorEventListener;
+import brooklyn.event.feed.ConfigToAttributes;
+import brooklyn.event.feed.function.FunctionFeed;
+import brooklyn.event.feed.function.FunctionPollConfig;
+import brooklyn.event.feed.http.HttpFeed;
+import brooklyn.event.feed.http.HttpPollConfig;
+import brooklyn.location.basic.SshMachineLocation;
+import brooklyn.policy.PolicySpec;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.net.Networking;
+
+import com.google.common.base.Functions;
+
+/**
+ * @see SimulatedJBoss7ServerImpl for description of purpose and configuration options.
+ */
+public class SimulatedNginxControllerImpl extends NginxControllerImpl {
+
+    public static final ConfigKey<Boolean> SIMULATE_ENTITY = SimulatedTheeTierApp.SIMULATE_ENTITY;
+    public static final ConfigKey<Boolean> SIMULATE_EXTERNAL_MONITORING = SimulatedTheeTierApp.SIMULATE_EXTERNAL_MONITORING;
+    public static final ConfigKey<Boolean> SKIP_SSH_ON_START = SimulatedTheeTierApp.SKIP_SSH_ON_START;
+    
+    private HttpFeed httpFeed;
+    private FunctionFeed functionFeed;
+    
+    @Override
+    public Class<?> getDriverInterface() {
+        return SimulatedNginxSshDriver.class;
+    }
+
+    @Override
+    public void connectSensors() {
+        boolean simulateEntity = getConfig(SIMULATE_ENTITY);
+        boolean simulateExternalMonitoring = getConfig(SIMULATE_EXTERNAL_MONITORING);
+
+        if (!simulateEntity && !simulateExternalMonitoring) {
+            super.connectSensors();
+            return;
+        }
+
+        // From AbstractController.connectSensors
+        if (getUrl()==null) {
+            setAttribute(MAIN_URI, URI.create(inferUrl()));
+            setAttribute(ROOT_URL, inferUrl());
+        }
+        addServerPoolMemberTrackingPolicy();
+
+        // From NginxController.connectSensors
+        ConfigToAttributes.apply(this);
+
+        if (!simulateExternalMonitoring) {
+            // if simulating entity, then simulate work of periodic HTTP request; TODO but not parsing JSON response
+            String uriToPoll = (simulateEntity) ? "http://localhost:8081" : getAttribute(MAIN_URI).toString();
+            
+            httpFeed = HttpFeed.builder()
+                    .entity(this)
+                    .period(getConfig(HTTP_POLL_PERIOD))
+                    .baseUri(uriToPoll)
+                    .poll(new HttpPollConfig<Boolean>(SERVICE_UP)
+                            .onSuccess(Functions.constant(true))
+                            .onFailureOrException(Functions.constant(true)))
+                    .build();
+        }
+        
+        functionFeed = FunctionFeed.builder()
+                .entity(this)
+                .period(getConfig(HTTP_POLL_PERIOD))
+                .poll(new FunctionPollConfig<Boolean,Boolean>(SERVICE_UP)
+                        .callable(new Callable<Boolean>() {
+                            public Boolean call() {
+                                return true;
+                            }}))
+                .build();
+
+        // Can guarantee that parent/managementContext has been set
+        Group urlMappings = getConfig(URL_MAPPINGS);
+        if (urlMappings != null) {
+            // Listen to the targets of each url-mapping changing
+            subscribeToMembers(urlMappings, UrlMapping.TARGET_ADDRESSES, new SensorEventListener<Collection<String>>() {
+                    @Override public void onEvent(SensorEvent<Collection<String>> event) {
+                        updateNeeded();
+                    }
+                });
+
+            // Listen to url-mappings being added and removed
+            urlMappingsMemberTrackerPolicy = addPolicy(PolicySpec.create(UrlMappingsMemberTrackerPolicy.class)
+                    .configure("group", urlMappings));
+        }
+    }
+
+    @Override
+    protected void disconnectSensors() {
+        super.disconnectSensors();
+        if (httpFeed != null) httpFeed.stop();
+        if (functionFeed != null) functionFeed.stop();
+    }
+
+    public static class SimulatedNginxSshDriver extends NginxSshDriver {
+        public SimulatedNginxSshDriver(SimulatedNginxControllerImpl entity, SshMachineLocation machine) {
+            super(entity, machine);
+        }
+
+        @Override
+        public void install() {
+            if (entity.getConfig(SKIP_SSH_ON_START)) {
+                // no-op
+            } else {
+                super.install();
+            }
+        }
+        
+        @Override
+        public void customize() {
+            if (entity.getConfig(SKIP_SSH_ON_START)) {
+                // no-op
+            } else {
+                super.customize();
+            }
+        }
+        
+        @Override
+        public void launch() {
+            if (!entity.getConfig(SIMULATE_ENTITY)) {
+                super.launch();
+                return;
+            }
+            
+            Networking.checkPortsValid(MutableMap.of("httpPort", getPort()));
+
+            if (entity.getConfig(SKIP_SSH_ON_START)) {
+                // minimal ssh, so that isRunning will subsequently work
+                newScript(MutableMap.of("usePidFile", getPidFile()), LAUNCHING)
+                        .body.append(
+                                format("mkdir -p %s/logs", getRunDir()),
+                                format("nohup sleep 100000 > %s 2>&1 < /dev/null &", getLogFileLocation()))
+                        .execute();
+            } else {
+                newScript(MutableMap.of("usePidFile", false), LAUNCHING)
+                        .body.append(
+                                format("cd %s", getRunDir()),
+                                "echo skipping exec of requireExecutable ./sbin/nginx",
+                                sudoBashCIfPrivilegedPort(getPort(), format(
+                                        "echo skipping exec of nohup ./sbin/nginx -p %s/ -c conf/server.conf > %s 2>&1 &", getRunDir(), getLogFileLocation())),
+                                format("nohup sleep 100000 > %s 2>&1 < /dev/null &", getLogFileLocation()),
+                                format("echo $! > "+getPidFile()),
+                                format("for i in {1..10}\n" +
+                                        "do\n" +
+                                        "    test -f %1$s && ps -p `cat %1$s` && exit\n" +
+                                        "    sleep 1\n" +
+                                        "done\n" +
+                                        "echo \"No explicit error launching nginx but couldn't find process by pid; continuing but may subsequently fail\"\n" +
+                                        "cat %2$s | tee /dev/stderr",
+                                        getPidFile(), getLogFileLocation()))
+                        .execute();
+            }
+        }
+
+        // Use pid file, because just simulating the run of nginx
+        @Override
+        public void stop() {
+            newScript(MutableMap.of("usePidFile", getPidFile()), STOPPING).execute();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bd44bb8f/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedTheeTierApp.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedTheeTierApp.java b/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedTheeTierApp.java
new file mode 100644
index 0000000..273d130
--- /dev/null
+++ b/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedTheeTierApp.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.qa.load;
+
+import static brooklyn.event.basic.DependentConfiguration.attributeWhenReady;
+import static brooklyn.event.basic.DependentConfiguration.formatString;
+
+import java.util.Collection;
+import java.util.List;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.enricher.Enrichers;
+import brooklyn.enricher.HttpLatencyDetector;
+import brooklyn.entity.basic.AbstractApplication;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.StartableApplication;
+import brooklyn.entity.database.mysql.MySqlNode;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.java.JavaEntityMethods;
+import brooklyn.entity.proxy.nginx.NginxController;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.entity.trait.Startable;
+import brooklyn.entity.webapp.ControlledDynamicWebAppCluster;
+import brooklyn.entity.webapp.DynamicWebAppCluster;
+import brooklyn.entity.webapp.JavaWebAppService;
+import brooklyn.entity.webapp.WebAppService;
+import brooklyn.entity.webapp.WebAppServiceConstants;
+import brooklyn.entity.webapp.jboss.JBoss7Server;
+import brooklyn.launcher.BrooklynLauncher;
+import brooklyn.location.basic.PortRanges;
+import brooklyn.policy.autoscaling.AutoScalerPolicy;
+import brooklyn.util.CommandLineUtil;
+import brooklyn.util.collections.MutableSet;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+
+/**
+ * A 3-tier app where all components are just "simulated" - they don't actually run 
+ * real app-servers or databases, instead just executing a "sleep" command to simulate 
+ * the running process.
+ * 
+ * This is useful for load testing, where we want to test the performance of Brooklyn
+ * rather than the ability to host many running app-servers.
+ * 
+ * The app is based on WebClusterDatabaseExampleApp
+ * 
+ * @see SimulatedJBoss7ServerImpl for description of purpose and configuration options.
+ */
+public class SimulatedTheeTierApp extends AbstractApplication {
+
+    public static final ConfigKey<Boolean> SIMULATE_ENTITY = ConfigKeys.newBooleanConfigKey("simulateEntity", "", true);
+    
+    public static final ConfigKey<Boolean> SIMULATE_EXTERNAL_MONITORING = ConfigKeys.newBooleanConfigKey("simulateExternalMonitoring", "", true);
+
+    public static final ConfigKey<Boolean> SKIP_SSH_ON_START = ConfigKeys.newBooleanConfigKey("skipSshOnStart", "", false);
+
+    public static final String WAR_PATH = "classpath://hello-world.war";
+    public static final String DB_TABLE = "visitors";
+    public static final String DB_USERNAME = "brooklyn";
+    public static final String DB_PASSWORD = "br00k11n";
+    public static final boolean USE_HTTPS = false;
+    
+    @Override
+    public void init() {
+        MySqlNode mysql = addChild(
+                EntitySpec.create(MySqlNode.class)
+                .impl(SimulatedMySqlNodeImpl.class));
+
+        ControlledDynamicWebAppCluster web = addChild(
+                EntitySpec.create(ControlledDynamicWebAppCluster.class)
+                .configure(ControlledDynamicWebAppCluster.MEMBER_SPEC, EntitySpec.create(JBoss7Server.class).impl(SimulatedJBoss7ServerImpl.class))
+                .configure(ControlledDynamicWebAppCluster.CONTROLLER_SPEC, EntitySpec.create(NginxController.class).impl(SimulatedNginxControllerImpl.class))
+                        .configure(WebAppService.HTTP_PORT, PortRanges.fromString("8080+"))
+                        .configure(JavaWebAppService.ROOT_WAR, WAR_PATH)
+                        .configure(JavaEntityMethods.javaSysProp("brooklyn.example.db.url"), 
+                                formatString("jdbc:%s%s?user=%s\\&password=%s", 
+                                        attributeWhenReady(mysql, MySqlNode.DATASTORE_URL), DB_TABLE, DB_USERNAME, DB_PASSWORD))
+                        .configure(DynamicCluster.INITIAL_SIZE, 2)
+                        .configure(WebAppService.ENABLED_PROTOCOLS, ImmutableSet.of(USE_HTTPS ? "https" : "http")) );
+
+        web.getCluster().addPolicy(AutoScalerPolicy.builder().
+                metric(DynamicWebAppCluster.REQUESTS_PER_SECOND_IN_WINDOW_PER_NODE).
+                metricRange(10, 100).
+                sizeRange(2, 5).
+                build());
+
+        addEnricher(Enrichers.builder()
+                .propagating(Attributes.MAIN_URI, WebAppServiceConstants.ROOT_URL,
+                        DynamicWebAppCluster.REQUESTS_PER_SECOND_IN_WINDOW,
+                        HttpLatencyDetector.REQUEST_LATENCY_IN_SECONDS_IN_WINDOW)
+                .from(web)
+                .build());
+        
+        addEnricher(Enrichers.builder()
+                .aggregating(Startable.SERVICE_UP)
+                .publishing(Startable.SERVICE_UP)
+                .fromHardcodedProducers(ImmutableList.of(web, mysql))
+                .computing(new Function<Collection<Boolean>, Boolean>() {
+                    @Override public Boolean apply(Collection<Boolean> input) {
+                        return input != null && input.size() == 2 && MutableSet.copyOf(input).equals(ImmutableSet.of(true));
+                    }})
+                .build());
+    }
+    
+    public static void main(String[] argv) {
+        List<String> args = Lists.newArrayList(argv);
+        String port =  CommandLineUtil.getCommandLineOption(args, "--port", "8081+");
+        String location = CommandLineUtil.getCommandLineOption(args, "--location", "localhost");
+
+        BrooklynLauncher launcher = BrooklynLauncher.newInstance()
+                 .application(EntitySpec.create(StartableApplication.class, SimulatedTheeTierApp.class)
+                         .displayName("Brooklyn WebApp Cluster with Database example"))
+                 .webconsolePort(port)
+                 .location(location)
+                 .start();
+             
+        Entities.dumpInfo(launcher.getApplications());
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bd44bb8f/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/Monitor.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/Monitor.java b/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/Monitor.java
new file mode 100644
index 0000000..7a3ea42
--- /dev/null
+++ b/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/Monitor.java
@@ -0,0 +1,261 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.qa.longevity;
+
+import static org.apache.brooklyn.qa.longevity.StatusRecorder.Factory.chain;
+import static org.apache.brooklyn.qa.longevity.StatusRecorder.Factory.noop;
+import static org.apache.brooklyn.qa.longevity.StatusRecorder.Factory.toFile;
+import static org.apache.brooklyn.qa.longevity.StatusRecorder.Factory.toLog;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import joptsimple.OptionParser;
+import joptsimple.OptionSet;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.util.collections.TimeWindowedList;
+import brooklyn.util.collections.TimestampedValue;
+import brooklyn.util.time.Duration;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Range;
+import com.google.common.io.Files;
+
+public class Monitor {
+
+    private static final Logger LOG = LoggerFactory.getLogger(Monitor.class);
+    
+    private static final int checkPeriodMs = 1000;
+
+    private static final OptionParser parser = new OptionParser() {
+        {
+            acceptsAll(ImmutableList.of("help", "?", "h"), "show help");
+            accepts("webUrl", "Web-app url")
+                    .withRequiredArg().ofType(URL.class);
+            accepts("brooklynPid", "Brooklyn pid")
+                    .withRequiredArg().ofType(Integer.class);
+            accepts("logFile", "Brooklyn log file")
+                    .withRequiredArg().ofType(File.class);
+            accepts("logGrep", "Grep in log file (defaults to 'SEVERE|ERROR|WARN|Exception|Error'")
+                    .withRequiredArg().ofType(String.class);
+            accepts("logGrepExclusionsFile", "File of expressions to be ignored in log file")
+                    .withRequiredArg().ofType(File.class);
+            accepts("webProcesses", "Name (for `ps ax | grep` of web-processes")
+                    .withRequiredArg().ofType(String.class);
+            accepts("numWebProcesses", "Number of web-processes expected (e.g. 1 or 1-3)")
+                    .withRequiredArg().ofType(String.class);
+            accepts("webProcessesCyclingPeriod", "The period (in seconds) for cycling through the range of numWebProcesses")
+                    .withRequiredArg().ofType(Integer.class);
+            accepts("outFile", "File to write monitor status info")
+                    .withRequiredArg().ofType(File.class);
+            accepts("abortOnError", "Exit the JVM on error, with exit code 1")
+                    .withRequiredArg().ofType(Boolean.class);
+        }
+    };
+
+    public static void main(String[] argv) throws InterruptedException, IOException {
+        OptionSet options = parse(argv);
+
+        if (options == null || options.has("help")) {
+            parser.printHelpOn(System.out);
+            System.exit(0);
+        }
+
+        MonitorPrefs prefs = new MonitorPrefs();
+        prefs.webUrl = options.hasArgument("webUrl") ? (URL) options.valueOf("webUrl") : null;
+        prefs.brooklynPid = options.hasArgument("brooklynPid") ? (Integer) options.valueOf("brooklynPid") : -1;
+        prefs.logFile = options.hasArgument("logFile") ? (File) options.valueOf("logFile") : null;
+        prefs.logGrep = options.hasArgument("logGrep") ? (String) options.valueOf("logGrep") : "SEVERE|ERROR|WARN|Exception|Error";
+        prefs.logGrepExclusionsFile = options.hasArgument("logGrepExclusionsFile") ? (File) options.valueOf("logGrepExclusionsFile") : null;
+        prefs.webProcessesRegex = options.hasArgument("webProcesses") ? (String) options.valueOf("webProcesses") : null;
+        prefs.numWebProcesses = options.hasArgument("numWebProcesses") ? parseRange((String) options.valueOf("numWebProcesses")) : null;
+        prefs.webProcessesCyclingPeriod = options.hasArgument("webProcessesCyclingPeriod") ? (Integer) options.valueOf("webProcessesCyclingPeriod") : -1;
+        prefs.outFile = options.hasArgument("outFile") ? (File) options.valueOf("outFile") : null;
+        prefs.abortOnError = options.hasArgument("abortOnError") ? (Boolean) options.valueOf("abortOnError") : false;
+        Monitor main = new Monitor(prefs, MonitorListener.NOOP);
+        main.start();
+    }
+
+    private static Range<Integer> parseRange(String range) {
+        if (range.contains("-")) {
+            String[] parts = range.split("-");
+            return Range.closed(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
+        } else {
+            return Range.singleton(Integer.parseInt(range));
+        }
+    }
+    
+    private static OptionSet parse(String...argv) {
+        try {
+            return parser.parse(argv);
+        } catch (Exception e) {
+            System.out.println("Error in parsing options: " + e.getMessage());
+            return null;
+        }
+    }
+
+    private final MonitorPrefs prefs;
+    private final StatusRecorder recorder;
+    private final MonitorListener listener;
+    
+    public Monitor(MonitorPrefs prefs, MonitorListener listener) {
+        this.prefs = prefs;
+        this.listener = listener;
+        this.recorder = chain(toLog(LOG), (prefs.outFile != null ? toFile(prefs.outFile) : noop()));
+    }
+
+    private void start() throws IOException {
+        LOG.info("Monitoring: "+prefs);
+        ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
+
+        final AtomicReference<List<String>> previousLogLines = new AtomicReference<List<String>>(Collections.<String>emptyList());
+        final TimeWindowedList<Integer> numWebProcessesHistory = new TimeWindowedList<Integer>(
+                ImmutableMap.of("timePeriod", Duration.seconds(prefs.webProcessesCyclingPeriod), "minExpiredVals", 1));
+        final Set<String> logGrepExclusions = ImmutableSet.copyOf(Files.readLines(prefs.logGrepExclusionsFile, Charsets.UTF_8));
+        
+        executor.scheduleAtFixedRate(new Runnable() {
+                @Override public void run() {
+                    StatusRecorder.Record record = new StatusRecorder.Record();
+                    StringBuilder failureMsg = new StringBuilder();
+                    try {
+                        if (prefs.brooklynPid > 0) {
+                            boolean pidRunning = MonitorUtils.isPidRunning(prefs.brooklynPid, "java");
+                            MonitorUtils.MemoryUsage memoryUsage = MonitorUtils.getMemoryUsage(prefs.brooklynPid, ".*brooklyn.*", 1000);
+                            record.put("pidRunning", pidRunning);
+                            record.put("totalMemoryBytes", memoryUsage.getTotalMemoryBytes());
+                            record.put("totalMemoryInstances", memoryUsage.getTotalInstances());
+                            record.put("instanceCounts", memoryUsage.getInstanceCounts());
+                            
+                            if (!pidRunning) {
+                                failureMsg.append("pid "+prefs.brooklynPid+" is not running"+"\n");
+                            }
+                        }
+                        if (prefs.webUrl != null) {
+                            boolean webUrlUp = MonitorUtils.isUrlUp(prefs.webUrl);
+                            record.put("webUrlUp", webUrlUp);
+                            
+                            if (!webUrlUp) {
+                                failureMsg.append("web URL "+prefs.webUrl+" is not available"+"\n");
+                            }
+                        }
+                        if (prefs.logFile != null) {
+                            List<String> logLines = MonitorUtils.searchLog(prefs.logFile, prefs.logGrep, logGrepExclusions);
+                            List<String> newLogLines = getAdditions(previousLogLines.get(), logLines);
+                            previousLogLines.set(logLines);
+                            record.put("logLines", newLogLines);
+                            
+                            if (newLogLines.size() > 0) {
+                                failureMsg.append("Log contains warnings/errors: "+newLogLines+"\n");
+                            }
+                        }
+                        if (prefs.webProcessesRegex != null) {
+                            List<Integer> pids = MonitorUtils.getRunningPids(prefs.webProcessesRegex, "--webProcesses");
+                            pids.remove((Object)MonitorUtils.findOwnPid());
+                            
+                            record.put("webPids", pids);
+                            record.put("numWebPids", pids.size());
+                            numWebProcessesHistory.add(pids.size());
+                            
+                            if (prefs.numWebProcesses != null) {
+                                boolean numWebPidsInRange = prefs.numWebProcesses.apply(pids.size());
+                                record.put("numWebPidsInRange", numWebPidsInRange);
+                                
+                                if (!numWebPidsInRange) {
+                                    failureMsg.append("num web processes out-of-range: pids="+pids+"; size="+pids.size()+"; expected="+prefs.numWebProcesses);
+                                }
+                            
+                                if (prefs.webProcessesCyclingPeriod > 0) {
+                                    List<TimestampedValue<Integer>> values = numWebProcessesHistory.getValues();
+                                    long valuesTimeRange = (values.get(values.size()-1).getTimestamp() - values.get(0).getTimestamp());
+                                    if (values.size() > 0 && valuesTimeRange > SECONDS.toMillis(prefs.webProcessesCyclingPeriod)) {
+                                        int min = -1;
+                                        int max = -1;
+                                        for (TimestampedValue<Integer> val : values) {
+                                            min = (min < 0) ? val.getValue() : Math.min(val.getValue(), min);
+                                            max = Math.max(val.getValue(), max);
+                                        }
+                                        record.put("minWebSizeInPeriod", min);
+                                        record.put("maxWebSizeInPeriod", max);
+                                        
+                                        if (min > prefs.numWebProcesses.lowerEndpoint() || max < prefs.numWebProcesses.upperEndpoint()) {
+                                            failureMsg.append("num web processes not increasing/decreasing correctly: " +
+                                                    "pids="+pids+"; size="+pids.size()+"; cyclePeriod="+prefs.webProcessesCyclingPeriod+
+                                                    "; expectedRange="+prefs.numWebProcesses+"; min="+min+"; max="+max+"; history="+values);
+                                        }
+                                    } else {
+                                        int numVals = values.size();
+                                        long startTime = (numVals > 0) ? values.get(0).getTimestamp() : 0;
+                                        long endTime = (numVals > 0) ? values.get(values.size()-1).getTimestamp() : 0;
+                                        LOG.info("Insufficient vals in time-window to determine cycling behaviour over period ("+prefs.webProcessesCyclingPeriod+"secs): "+
+                                                "numVals="+numVals+"; startTime="+startTime+"; endTime="+endTime+"; periodCovered="+(endTime-startTime)/1000);
+                                    }
+                                }
+                            }
+                        }
+                        
+                    } catch (Throwable t) {
+                        LOG.error("Error during periodic checks", t);
+                        throw Throwables.propagate(t);
+                    }
+                    
+                    try {
+                        recorder.record(record);
+                        listener.onRecord(record);
+                        
+                        if (failureMsg.length() > 0) {
+                            listener.onFailure(record, failureMsg.toString());
+                            
+                            if (prefs.abortOnError) {
+                                LOG.error("Aborting on error: "+failureMsg);
+                                System.exit(1);
+                            }
+                        }
+                        
+                    } catch (Throwable t) {
+                        LOG.warn("Error recording monitor info ("+record+")", t);
+                        throw Throwables.propagate(t);
+                    }
+                }
+            }, 0, checkPeriodMs, TimeUnit.MILLISECONDS);
+    }
+    
+    // TODO What is the guava equivalent? Don't want Set.difference, because duplicates/ordered.
+    private static List<String> getAdditions(List<String> prev, List<String> next) {
+        List<String> result = Lists.newArrayList(next);
+        result.removeAll(prev);
+        return result;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bd44bb8f/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/MonitorListener.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/MonitorListener.java b/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/MonitorListener.java
new file mode 100644
index 0000000..11fdd3f
--- /dev/null
+++ b/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/MonitorListener.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.qa.longevity;
+
+import org.apache.brooklyn.qa.longevity.StatusRecorder.Record;
+
+public interface MonitorListener {
+
+    public static final MonitorListener NOOP = new MonitorListener() {
+        @Override public void onRecord(Record record) {
+        }
+        @Override public void onFailure(Record record, String msg) {
+        }
+    };
+    
+    public void onRecord(Record record);
+    
+    public void onFailure(Record record, String msg);
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bd44bb8f/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/MonitorPrefs.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/MonitorPrefs.java b/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/MonitorPrefs.java
new file mode 100644
index 0000000..4d74045
--- /dev/null
+++ b/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/MonitorPrefs.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.qa.longevity;
+
+import java.io.File;
+import java.net.URL;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.Range;
+
+public class MonitorPrefs {
+
+    public URL webUrl;
+    public int brooklynPid;
+    public File logFile;
+    public String logGrep;
+    public File logGrepExclusionsFile;
+    public String webProcessesRegex;
+    public Range<Integer> numWebProcesses;
+    public int webProcessesCyclingPeriod;
+    public File outFile;
+    public boolean abortOnError;
+    
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(this)
+                .add("webUrl", webUrl)
+                .add("brooklynPid", brooklynPid)
+                .add("logFile", logFile)
+                .add("logGrep", logGrep)
+                .add("logGrepExclusionsFile", logGrepExclusionsFile)
+                .add("outFile", outFile)
+                .add("webProcessesRegex", webProcessesRegex)
+                .add("numWebProcesses", numWebProcesses)
+                .add("webProcessesCyclingPeriod", webProcessesCyclingPeriod)
+                .toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bd44bb8f/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/MonitorUtils.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/MonitorUtils.java b/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/MonitorUtils.java
new file mode 100644
index 0000000..54acb3b
--- /dev/null
+++ b/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/MonitorUtils.java
@@ -0,0 +1,329 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.qa.longevity;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.util.http.HttpTool;
+import brooklyn.util.http.HttpToolResponse;
+import brooklyn.util.stream.StreamGobbler;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.io.ByteStreams;
+
+public class MonitorUtils {
+
+    private static final Logger LOG = LoggerFactory.getLogger(MonitorUtils.class);
+
+    private static volatile int ownPid = -1;
+
+    /**
+     * Confirm can read from URL.
+     *
+     * @param url
+     */
+    public static boolean isUrlUp(URL url) {
+        try {
+            HttpToolResponse result = HttpTool.httpGet(
+                    HttpTool.httpClientBuilder().trustAll().build(), 
+                    URI.create(url.toString()), 
+                    ImmutableMap.<String,String>of());
+            int statuscode = result.getResponseCode();
+
+            if (statuscode != 200) {
+                LOG.info("Error reading URL {}: {}, {}", new Object[]{url, statuscode, result.getReasonPhrase()});
+                return false;
+            } else {
+                return true;
+            }
+        } catch (Exception e) {
+            LOG.info("Error reading URL {}: {}", url, e);
+            return false;
+        }
+    }
+
+    public static boolean isPidRunning(int pid) {
+        return isPidRunning(pid, null);
+    }
+
+    /**
+     * Confirm the given pid is running, and that the the process matches the given regex.
+     *
+     * @param pid
+     * @param regex
+     */
+    public static boolean isPidRunning(int pid, String regex) {
+        Process process = exec("ps -p " + pid);
+        String out = waitFor(process);
+         if (process.exitValue() > 0) {
+            String err = toString(process.getErrorStream());
+            LOG.info(String.format("pid %s not running: %s", pid, err));
+            return false;
+        }
+
+        if (regex != null) {
+            String regex2 = "^\\s*" + pid + ".*" + regex;
+            boolean found = false;
+            for (String line : out.split("\n")) {
+                if (hasAtLeastOneMatch(line, regex2)) {
+                    found = true;
+                    break;
+                }
+            }
+
+            if (!found) {
+                String txt = toString(process.getInputStream());
+                LOG.info("process did not match regular expression: "+txt);
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private static boolean hasAtLeastOneMatch(String line, String regex) {
+        return Pattern.matches(".*"+regex+".*", line);
+    }
+
+    private static String toString(InputStream in){
+        try {
+            byte[] bytes = ByteStreams.toByteArray(in);
+            return new String(bytes);
+        } catch (IOException e) {
+            throw Throwables.propagate(e);
+        }
+
+    }
+
+    public static List<Integer> getRunningPids(String regex) {
+        return getRunningPids(regex, null);
+    }
+
+    /**
+     * Confirm the given pid is running, and that the the process matches the given regex.
+     *
+     * @param regex
+     * @param excludingRegex
+     */
+    public static List<Integer> getRunningPids(String regex, String excludingRegex) {
+        Process process = exec("ps ax");
+        String out = waitFor(process);
+
+        List<Integer> result = new LinkedList<Integer>();
+        for (String line : out.split("\n")) {
+            if (excludingRegex != null && hasAtLeastOneMatch(line, excludingRegex)) {
+                continue;
+            }
+            if (hasAtLeastOneMatch(line, regex)) {
+                String[] linesplit = line.trim().split("\\s+");
+                result.add(Integer.parseInt(linesplit[0]));
+            }
+        }
+        return result;
+    }
+
+    public static MemoryUsage getMemoryUsage(int pid){
+          return getMemoryUsage(pid, null,0);
+    }
+
+    /**
+     * @param pid
+     */
+    public static MemoryUsage getMemoryUsage(int pid, String clazzRegexOfInterest, int minInstancesOfInterest) {
+        Process process = exec(String.format("jmap -histo %s", pid));
+        String out = waitFor(process);
+
+        Map<String, Integer> instanceCounts = Maps.newLinkedHashMap();
+        long totalInstances=0;
+        long totalMemoryBytes=0;
+
+        for (String line : out.split("\n")) {
+            if (clazzRegexOfInterest!=null && hasAtLeastOneMatch(line, clazzRegexOfInterest)) {
+                // Format is:
+                //   num     #instances         #bytes  class name
+                //   1:           43506        8047096  example.MyClazz
+
+                String[] parts = line.trim().split("\\s+");
+                String clazz = parts[3];
+                int instanceCount = Integer.parseInt(parts[1]);
+                if (instanceCount >= minInstancesOfInterest) {
+                    instanceCounts.put(clazz, instanceCount);
+                }
+            }
+            if (hasAtLeastOneMatch(line, "^Total.*")) {
+                String[] parts = line.split("\\s+");
+                totalInstances = Long.parseLong(parts[1]);
+                totalMemoryBytes = Long.parseLong(parts[2]);
+            }
+        }
+
+        return new MemoryUsage(totalInstances, totalMemoryBytes, instanceCounts);
+    }
+
+    public static class MemoryUsage {
+        final long totalInstances;
+        final long totalMemoryBytes;
+        final Map<String, Integer> instanceCounts;
+
+        MemoryUsage(long totalInstances, long totalMemoryBytes, Map<String, Integer> instanceCounts) {
+            this.totalInstances = totalInstances;
+            this.totalMemoryBytes = totalMemoryBytes;
+            this.instanceCounts = instanceCounts;
+        }
+
+        public String toString() {
+            return Objects.toStringHelper(this)
+                    .add("totalInstances", totalInstances)
+                    .add("totalMemoryBytes", totalMemoryBytes)
+                    .add("instanceCounts", instanceCounts)
+                    .toString();
+        }
+
+        public long getTotalInstances() {
+            return totalInstances;
+        }
+
+        public long getTotalMemoryBytes() {
+            return totalMemoryBytes;
+        }
+
+        public Map<String, Integer> getInstanceCounts() {
+            return instanceCounts;
+        }
+    }
+
+    public static List<String> searchLog(File file, String grepOfInterest) {
+        return searchLog(file, grepOfInterest, new LinkedHashSet<String>());
+    }
+
+    /**
+     * Find lines in the given file that match given given regex.
+     *
+     * @param file
+     * @param grepOfInterest
+     */
+    public static List<String> searchLog(File file, String grepOfInterest, Set<String> grepExclusions) {
+        Process process = exec(String.format("grep -E %s %s", grepOfInterest, file.getAbsoluteFile()));
+        String out = waitFor(process);
+
+        // TODO Annoying that String.split() returns size 1 when empty string; lookup javadoc when back online...
+        if (out.length() == 0) return Collections.<String>emptyList();
+
+        List<String> result = new ArrayList<String>();
+        for (String line : out.trim().split("\n")) {
+            boolean excluded = false;
+            for (String exclusion : grepExclusions) {
+                if (!isNullOrEmpty(exclusion) && hasAtLeastOneMatch(line, exclusion)) {
+                    excluded = true;
+                }
+            }
+            if (!excluded) {
+                result.add(line);
+            }
+        }
+        return result;
+    }
+
+    public static Process exec(String cmd) {
+        LOG.info("executing cmd: " + cmd);
+
+        try {
+            return Runtime.getRuntime().exec(cmd);
+        } catch (IOException e) {
+            throw Throwables.propagate(e);
+        }
+    }
+
+    public static class ProcessHasStderr extends IllegalStateException {
+        private static final long serialVersionUID = -937871002993888405L;
+        
+        byte[] stderrBytes;
+        public ProcessHasStderr(byte[] stderrBytes) {
+            this("Process printed to stderr: " + new String(stderrBytes), stderrBytes);
+        }
+        public ProcessHasStderr(String message, byte[] stderrBytes) {
+            super(message);
+            this.stderrBytes = stderrBytes;
+        }
+    }
+    
+    /**
+     * Waits for the given process to complete, consuming its stdout and returning it as a string.
+     * If there is any output on stderr an exception will be thrown.
+     */
+    public static String waitFor(Process process) {
+        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+        @SuppressWarnings("resource") //Closeable doesn't seem appropriate for StreamGobbler since it isn't expected to be called every time
+        StreamGobbler gobblerOut = new StreamGobbler(process.getInputStream(), bytesOut, null);
+        gobblerOut.start();
+
+        ByteArrayOutputStream bytesErr = new ByteArrayOutputStream();
+        @SuppressWarnings("resource")
+        StreamGobbler gobblerErr = new StreamGobbler(process.getErrorStream(), bytesErr, null);
+        gobblerErr.start();
+
+        try {
+            process.waitFor();
+            gobblerOut.blockUntilFinished();
+            gobblerErr.blockUntilFinished();
+
+            if (bytesErr.size() > 0) {
+                throw new ProcessHasStderr(bytesErr.toByteArray());
+            }
+
+            return new String(bytesOut.toByteArray());
+        } catch (Exception e) {
+            throw Throwables.propagate(e);
+        } finally {
+            if (gobblerOut.isAlive()) gobblerOut.interrupt();
+            if (gobblerErr.isAlive()) gobblerErr.interrupt();
+        }
+    }
+
+    public static int findOwnPid() throws IOException {
+        if (ownPid >= 0) return ownPid;
+
+        String[] cmd = new String[]{"bash", "-c", "echo $PPID"};
+        Process process = Runtime.getRuntime().exec(cmd);
+        String out = MonitorUtils.waitFor(process);
+        ownPid = Integer.parseInt(out.trim());
+        return ownPid;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bd44bb8f/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/StatusRecorder.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/StatusRecorder.java b/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/StatusRecorder.java
new file mode 100644
index 0000000..210e0a2
--- /dev/null
+++ b/usage/qa/src/main/java/org/apache/brooklyn/qa/longevity/StatusRecorder.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.qa.longevity;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+
+import org.slf4j.Logger;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.Maps;
+import com.google.common.io.Files;
+
+public interface StatusRecorder {
+
+    public void record(Record record) throws IOException;
+    
+    public static class Factory {
+        public static final StatusRecorder NOOP = new StatusRecorder() {
+            @Override public void record(Record record) {}
+        };
+        
+        public static StatusRecorder noop() {
+            return NOOP;
+        }
+        public static StatusRecorder toFile(File outFile) {
+            return new FileBasedStatusRecorder(outFile);
+        }
+        public static StatusRecorder toSysout() {
+            return new SysoutBasedStatusRecorder();
+        }
+        public static StatusRecorder toLog(Logger log) {
+            return new LogBasedStatusRecorder(log);
+        }
+        public static StatusRecorder chain(StatusRecorder...recorders) {
+            return new ChainingStatusRecorder(recorders);
+        }
+    }
+    
+    public static class Record {
+        private final Map<String,Object> fields = Maps.newLinkedHashMap();
+        
+        public void putAll(Map<String,?> entries) {
+            fields.putAll(entries);
+        }
+        
+        public void putAll(String keyPrefix, Map<String,?> entries) {
+            for (Map.Entry<String,?> entry : entries.entrySet()) {
+                fields.put(keyPrefix+entry.getKey(), entry.getValue());
+            }
+        }
+        
+        public void put(String key, Object val) {
+            fields.put(key, val);
+        }
+        
+        @Override
+        public String toString() {
+            return fields.toString();
+        }
+    }
+    
+    public static class FileBasedStatusRecorder implements StatusRecorder {
+        private final File outFile;
+    
+        public FileBasedStatusRecorder(File outFile) {
+            this.outFile = outFile;
+        }
+        
+        @Override
+        public void record(Record record) throws IOException {
+            Files.append(record.fields.toString()+"\n", outFile, Charsets.UTF_8);
+        }
+    }
+    
+    public static class SysoutBasedStatusRecorder implements StatusRecorder {
+        public SysoutBasedStatusRecorder() {
+        }
+        
+        @Override
+        public void record(Record record) {
+            System.out.println(record.fields);
+        }
+    }
+    
+    public static class LogBasedStatusRecorder implements StatusRecorder {
+        private final Logger log;
+
+        public LogBasedStatusRecorder(Logger log) {
+            this.log = log;
+        }
+        
+        @Override
+        public void record(Record record) {
+            log.info("{}", record.fields);
+        }
+    }
+    
+    public static class ChainingStatusRecorder implements StatusRecorder {
+        private final StatusRecorder[] recorders;
+
+        public ChainingStatusRecorder(StatusRecorder... recorders) {
+            this.recorders = recorders;
+        }
+        
+        @Override
+        public void record(Record record) throws IOException {
+            for (StatusRecorder recorder : recorders) {
+                recorder.record(record);
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bd44bb8f/usage/qa/src/test/java/brooklyn/qa/load/LoadTest.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/test/java/brooklyn/qa/load/LoadTest.java b/usage/qa/src/test/java/brooklyn/qa/load/LoadTest.java
deleted file mode 100644
index 16a0195..0000000
--- a/usage/qa/src/test/java/brooklyn/qa/load/LoadTest.java
+++ /dev/null
@@ -1,243 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 brooklyn.qa.load;
-
-import static org.testng.Assert.assertEquals;
-
-import java.io.File;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.Entities;
-import brooklyn.entity.basic.StartableApplication;
-import brooklyn.entity.proxying.EntitySpec;
-import brooklyn.entity.rebind.persister.PersistMode;
-import brooklyn.entity.trait.Startable;
-import brooklyn.launcher.BrooklynLauncher;
-import brooklyn.location.Location;
-import brooklyn.management.ManagementContext;
-import brooklyn.management.ha.HighAvailabilityMode;
-import brooklyn.management.internal.LocalManagementContext;
-import brooklyn.test.PerformanceTestUtils;
-import brooklyn.util.os.Os;
-import brooklyn.util.time.Duration;
-
-import com.google.common.base.Stopwatch;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.common.io.Files;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
-
-/**
- * Customers ask about the scalability of Brooklyn. These load tests investigate how many 
- * concurrent apps can be deployed and managed by a single Brooklyn management node.
- * 
- * The apps are "simulated" in that they don't create the underlying resources 
- * (we are not checking if the test machine can run 100s of app-servers simultaneously!) 
- * The install/customize/launch will instead execute ssh commands of comparable length,
- * but that just echo rather than execute the actual commands.
- * 
- * "SIMULATE_EXTERNAL_MONITORING" means that we do not poll the entities directly (over ssh, http or 
- * whatever). Instead we simulate the metrics being injected directly to be set on the entity (e.g. 
- * having been collected from a Graphite server).
- * 
- * "SKIP_SSH_ON_START" means don't do the normal install+customize+launch ssh commands. Instead, just
- * startup the entities so we can monitor their resource usage.
- */
-public class LoadTest {
-
-    // TODO Could/should issue provisioning request through REST api, rather than programmatically; 
-    // and poll to detect completion.
-    
-    /*
-     * Useful commands when investigating:
-     *     LOG_FILE=usage/qa/brooklyn-camp-tests.log
-     *     grep -E "OutOfMemoryError|[P|p]rovisioning time|sleeping before|CPU fraction|LoadTest using" $LOG_FILE | less
-     *     grep -E "OutOfMemoryError|[P|p]rovisioning time" $LOG_FILE; grep "CPU fraction" $LOG_FILE | tail -1; grep "LoadTest using" $LOG_FILE | tail -1
-     *     grep -E "OutOfMemoryError|LoadTest using" $LOG_FILE
-     */
-    private static final Logger LOG = LoggerFactory.getLogger(LoadTest.class);
-
-    private File persistenceDir;
-    private BrooklynLauncher launcher;
-    private String webServerUrl;
-    private ManagementContext managementContext;
-    private ListeningExecutorService executor;
-    private Future<?> cpuFuture;
-    
-    private Location localhost;
-    
-    List<Duration> provisioningTimes;
-
-
-    @BeforeMethod(alwaysRun=true)
-    public void setUp() throws Exception {
-        // Create management node
-        persistenceDir = Files.createTempDir();
-        launcher = BrooklynLauncher.newInstance()
-                .persistMode(PersistMode.CLEAN)
-                .highAvailabilityMode(HighAvailabilityMode.MASTER)
-                .persistenceDir(persistenceDir)
-                .start();
-        webServerUrl = launcher.getServerDetails().getWebServerUrl();
-        managementContext = launcher.getServerDetails().getManagementContext();
-
-        localhost = managementContext.getLocationRegistry().resolve("localhost");
-        
-        provisioningTimes = Collections.synchronizedList(Lists.<Duration>newArrayList());
-
-        // Create executors
-        executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
-
-        // Monitor utilisation (memory/CPU) while tests run
-        executor.submit(new Callable<Void>() {
-            public Void call() {
-                try {
-                    while (true) {
-                        managementContext.getExecutionManager(); // force GC to be instantiated
-                        String usage = ((LocalManagementContext)managementContext).getGarbageCollector().getUsageString();
-                        LOG.info("LoadTest using "+usage);
-                        Thread.sleep(1000);
-                    }
-                } catch (InterruptedException e) {
-                    Thread.currentThread().interrupt(); // exit gracefully
-                } catch (Exception e) {
-                    LOG.error("Error getting usage info", e);
-                }
-                return null;
-            }});
-        
-        cpuFuture = PerformanceTestUtils.sampleProcessCpuTime(Duration.ONE_SECOND, "during testProvisionAppsConcurrently");
-
-    }
-    
-    @AfterMethod(alwaysRun=true)
-    public void tearDown() throws Exception {
-        if (cpuFuture != null) cpuFuture.cancel(true);
-        if (executor != null) executor.shutdownNow();
-        if (launcher != null) launcher.terminate();
-        if (persistenceDir != null) Os.deleteRecursively(persistenceDir);
-    }
-    
-    /**
-     * Creates multiple apps simultaneously. 
-     * 
-     * Long-term target is 50 concurrent provisioning requests (which may be issued while there are
-     * many existing applications under management). Until we reach that point, we can partition the
-     * load across multiple (separate) brooklyn management nodes.
-     *   TODO TBD: is that 50 VMs worth, or 50 apps with 4 VMs in each? 
-     * 
-     * TODO Does not measure the cost of jclouds for creating all the VMs/containers.
-     */
-    @Test(groups="Acceptance")
-    public void testLocalhostProvisioningAppsConcurrently() throws Exception {
-        final int NUM_CONCURRENT_APPS_PROVISIONING = 20; 
-        
-        List<ListenableFuture<StartableApplication>> futures = Lists.newArrayList();
-        for (int i = 0; i < NUM_CONCURRENT_APPS_PROVISIONING; i++) {
-            ListenableFuture<StartableApplication> future = executor.submit(newProvisionAppTask(managementContext, 
-                    EntitySpec.create(StartableApplication.class, SimulatedTheeTierApp.class)
-                            .configure(SimulatedTheeTierApp.SIMULATE_EXTERNAL_MONITORING, true)
-                            .displayName("Simulated app "+i)));
-            futures.add(future);
-        }
-        
-        List<StartableApplication> apps = Futures.allAsList(futures).get();
-        
-        for (StartableApplication app : apps) {
-            assertEquals(app.getAttribute(Startable.SERVICE_UP), (Boolean)true);
-        }
-    }
-    
-    /**
-     * Creates many apps, to monitor resource usage etc.
-     * 
-     * "SIMULATE_EXTERNAL_MONITORING" means that we do not poll the entities directly (over ssh, http or 
-     * whatever). Instead we simulate the metrics being injected directly to be set on the entity (e.g. 
-     * having been collected from a Graphite server).
-     * 
-     * Long-term target is 2500 VMs under management.
-     * Until we reach that point, we can partition the load across multiple (separate) brooklyn management nodes.
-     */
-    @Test(groups="Acceptance")
-    public void testLocalhostManyApps() throws Exception {
-        final int NUM_APPS = 630; // target is 2500 VMs; each blueprint has 4 (rounding up)
-        final int NUM_APPS_PER_BATCH = 10;
-        final int SLEEP_BETWEEN_BATCHES = 10*1000;
-        final boolean SKIP_SSH_ON_START = true; // getting ssh errors otherwise!
-        
-        int counter = 0;
-        
-        for (int i = 0; i < NUM_APPS / NUM_APPS_PER_BATCH; i++) {
-            List<ListenableFuture<StartableApplication>> futures = Lists.newArrayList();
-            for (int j = 0; j < NUM_APPS_PER_BATCH; j++) {
-                ListenableFuture<StartableApplication> future = executor.submit(newProvisionAppTask(
-                        managementContext, 
-                        EntitySpec.create(StartableApplication.class, SimulatedTheeTierApp.class)
-                                .configure(SimulatedTheeTierApp.SIMULATE_EXTERNAL_MONITORING, true)
-                                .configure(SimulatedTheeTierApp.SKIP_SSH_ON_START, SKIP_SSH_ON_START)
-                                .displayName("Simulated app "+(++counter))));
-                futures.add(future);
-            }
-            
-            List<StartableApplication> apps = Futures.allAsList(futures).get();
-            
-            for (StartableApplication app : apps) {
-                assertEquals(app.getAttribute(Startable.SERVICE_UP), (Boolean)true);
-            }
-
-            synchronized (provisioningTimes) {
-                LOG.info("cycle="+i+"; numApps="+counter+": provisioning times: "+provisioningTimes);
-                provisioningTimes.clear();
-            }
-
-            LOG.info("cycle="+i+"; numApps="+counter+": sleeping before next batch of apps");
-            Thread.sleep(SLEEP_BETWEEN_BATCHES);
-        }
-    }
-    
-    protected <T extends StartableApplication> Callable<T> newProvisionAppTask(final ManagementContext managementContext, final EntitySpec<T> entitySpec) {
-        return new Callable<T>() {
-            public T call() {
-                Stopwatch stopwatch = Stopwatch.createStarted();
-                T app = managementContext.getEntityManager().createEntity(entitySpec);
-                Entities.startManagement(app, managementContext);
-                app.start(ImmutableList.of(localhost));
-                Duration duration = Duration.of(stopwatch.elapsed(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS);
-                LOG.info("Provisioning time: "+duration);
-                provisioningTimes.add(duration);
-
-                return app;
-            }
-        };
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bd44bb8f/usage/qa/src/test/java/brooklyn/qa/longevity/MonitorUtilsTest.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/test/java/brooklyn/qa/longevity/MonitorUtilsTest.java b/usage/qa/src/test/java/brooklyn/qa/longevity/MonitorUtilsTest.java
deleted file mode 100644
index 2f1e854..0000000
--- a/usage/qa/src/test/java/brooklyn/qa/longevity/MonitorUtilsTest.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 brooklyn.qa.longevity;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertFalse;
-import static org.testng.Assert.assertTrue;
-import static org.testng.Assert.fail;
-
-import java.io.File;
-import java.io.IOException;
-import java.net.URL;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import org.testng.annotations.Test;
-
-import brooklyn.qa.longevity.MonitorUtils.ProcessHasStderr;
-import brooklyn.util.os.Os;
-import brooklyn.util.text.Strings;
-
-import com.google.common.base.Charsets;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.io.Files;
-
-public class MonitorUtilsTest {
-
-    @Test(enabled=false, timeOut=1000) // Demonstrates that process.waitFor() hangs for big output streams
-    public void testExecuteAndWaitFor() throws Exception {
-        Process process = createDumpingProcess(false);
-        process.waitFor();
-        fail("Should block while waiting to consume process output");
-    }
-
-    @Test(enabled=false, timeOut=1000) // Demonstrates that process.waitFor() hangs for big err streams
-    public void testExecuteAndWaitForErr() throws Exception {
-        Process process = createDumpingProcess(true);
-        process.waitFor();
-        fail("Should block while waiting to consume process output");
-    }
-
-    @Test(timeOut=1000)
-    public void testExecuteAndWaitForConsumingOutputStream() throws Exception {
-        Process process = createDumpingProcess(false);
-        String out = MonitorUtils.waitFor(process);
-        assertTrue(out.length() > 100000, "out.size="+out.length());
-    }
-
-    @Test(timeOut=1000, expectedExceptions=IllegalStateException.class)
-    public void testExecuteAndWaitForConsumingErrorStream() throws Exception {
-        Process process = createDumpingProcess(true);
-        MonitorUtils.waitFor(process);
-    }
-
-    private Process createDumpingProcess(boolean writeToErr) throws IOException {
-        String errSuffix = writeToErr ? " >&2" : "";
-        //Windows limits the length of the arguments so echo multiple times instead
-        String bigstr = Strings.repeat("a", 8000);
-        String bigcmd = Strings.repeat(getSilentPrefix() + "echo " + bigstr + errSuffix + Os.LINE_SEPARATOR, 15);
-        File file = Os.newTempFile("test-consume", ".bat");
-        file.setExecutable(true);
-        Files.write(bigcmd, file, Charsets.UTF_8);
-        Process process = MonitorUtils.exec(file.getAbsolutePath());
-        return process;
-    }
-
-    @Test(groups="UNIX")
-    public void testFindOwnPid() throws Exception {
-        int ownpid = MonitorUtils.findOwnPid();
-        assertTrue(ownpid > 0, "ownpid=$ownpid");
-        assertTrue(MonitorUtils.isPidRunning(ownpid, "java"),"java is not running");
-    }
-
-    @Test(groups="UNIX")
-    public void testIsPidRunning() throws Exception {
-        int usedPid = MonitorUtils.findOwnPid();
-
-        //the child process will terminate freeing it PID
-        String[] cmd = new String[]{"bash", "-c", "echo $$"};
-        Process process = Runtime.getRuntime().exec(cmd);
-        String out = MonitorUtils.waitFor(process);
-        int unusedPid = Integer.parseInt(out.trim());
-
-        assertTrue(MonitorUtils.isPidRunning(usedPid));
-        assertFalse(MonitorUtils.isPidRunning(unusedPid));
-        
-        try {
-            assertFalse(MonitorUtils.isPidRunning(1234567)); // too large
-        } catch (ProcessHasStderr e) {
-            // expected on osx
-        }
-    }
-
-    @Test(groups="UNIX")
-    public void testGetRunningPids() throws Exception {
-        int ownpid = MonitorUtils.findOwnPid();
-
-        List<Integer> javapids = MonitorUtils.getRunningPids("java");
-
-        assertTrue(javapids.contains(ownpid), "javapids="+javapids+"; ownpid="+ownpid);
-    }
-
-    @Test
-    public void testIsUrlUp() throws Exception {
-        assertFalse(MonitorUtils.isUrlUp(new URL("http://localhost/thispathdoesnotexist")));
-    }
-
-    @Test(groups="UNIX")
-    public void testSearchLog() throws Exception {
-        String fileContents = "line1\nline2\nline3\n";
-        File file = File.createTempFile("monitorUtilsTest.testSearchLog", ".txt");
-        Files.write(fileContents, file, Charsets.UTF_8);
-
-        try {
-            assertEquals(MonitorUtils.searchLog(file, "line1"), Arrays.asList("line1"));
-            assertEquals(MonitorUtils.searchLog(file, "line1|line2"), Arrays.asList("line1", "line2"));
-            assertEquals(MonitorUtils.searchLog(file, "textnotthere"), Collections.emptyList());
-            assertEquals(MonitorUtils.searchLog(file, "line"), Arrays.asList("line1", "line2", "line3"));
-        } finally {
-            file.delete();
-        }
-    }
-
-    @Test(groups="Integration")
-    public void testMemoryUsage() throws Exception {
-        int ownpid = MonitorUtils.findOwnPid();
-
-        MonitorUtils.MemoryUsage memUsage = MonitorUtils.getMemoryUsage(ownpid);
-        assertTrue(memUsage.totalInstances > 0, memUsage.toString());
-        assertTrue(memUsage.totalMemoryBytes > 0, memUsage.toString());
-        assertEquals(memUsage.getInstanceCounts(), Collections.emptyMap());
-
-        MonitorUtils.MemoryUsage memUsage2 = MonitorUtils.getMemoryUsage(ownpid, MonitorUtilsTest.class.getCanonicalName(),0);
-        assertEquals(memUsage2.getInstanceCounts(), ImmutableMap.of(MonitorUtilsTest.class.getCanonicalName(), 1));
-
-        MonitorUtils.MemoryUsage memUsage3 = MonitorUtils.getMemoryUsage(ownpid, MonitorUtilsTest.class.getCanonicalName(), 2);
-        assertEquals(memUsage3.getInstanceCounts(), Collections.emptyMap());
-    }
-
-    private String getSilentPrefix() {
-        if (Os.isMicrosoftWindows()) {
-            return "@";
-        } else {
-            return "";
-        }
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bd44bb8f/usage/qa/src/test/java/brooklyn/qa/longevity/webcluster/SinusoidalLoadGenerator.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/test/java/brooklyn/qa/longevity/webcluster/SinusoidalLoadGenerator.java b/usage/qa/src/test/java/brooklyn/qa/longevity/webcluster/SinusoidalLoadGenerator.java
deleted file mode 100644
index 392b976..0000000
--- a/usage/qa/src/test/java/brooklyn/qa/longevity/webcluster/SinusoidalLoadGenerator.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 brooklyn.qa.longevity.webcluster;
-
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import brooklyn.config.ConfigKey;
-import brooklyn.enricher.basic.AbstractEnricher;
-import brooklyn.entity.basic.ConfigKeys;
-import brooklyn.entity.basic.EntityLocal;
-import brooklyn.event.AttributeSensor;
-
-import com.google.common.base.Throwables;
-import com.google.common.reflect.TypeToken;
-
-/**
- * Periodically publishes values in the range of 0 to #amplitude. 
- * The value varies sinusoidally over time.
- */
-public class SinusoidalLoadGenerator extends AbstractEnricher {
-
-    private static final Logger LOG = LoggerFactory.getLogger(SinusoidalLoadGenerator.class);
-
-    public static final ConfigKey<AttributeSensor<Double>> TARGET = ConfigKeys.newConfigKey(new TypeToken<AttributeSensor<Double>>() {}, "target");
-    
-    public static final ConfigKey<Long> PUBLISH_PERIOD_MS = ConfigKeys.newLongConfigKey("publishPeriodMs");
-
-    public static final ConfigKey<Long> SIN_PERIOD_MS = ConfigKeys.newLongConfigKey("sinPeriodMs");
-
-    public static final ConfigKey<Double> SIN_AMPLITUDE = ConfigKeys.newDoubleConfigKey("sinAmplitude");
-
-    private final ScheduledExecutorService executor;
-    
-    public SinusoidalLoadGenerator() {
-        this.executor = Executors.newSingleThreadScheduledExecutor();
-    }
-    
-    public SinusoidalLoadGenerator(AttributeSensor<Double> target, long publishPeriodMs, long sinPeriodMs, double sinAmplitude) {
-        config().set(TARGET, target);
-        config().set(PUBLISH_PERIOD_MS, publishPeriodMs);
-        config().set(SIN_PERIOD_MS, sinPeriodMs);
-        config().set(SIN_AMPLITUDE, sinAmplitude);
-        this.executor = Executors.newSingleThreadScheduledExecutor();
-    }
-    
-    @Override
-    public void setEntity(final EntityLocal entity) {
-        super.setEntity(entity);
-        
-        executor.scheduleAtFixedRate(new Runnable() {
-            @Override public void run() {
-                try {
-                    long time = System.currentTimeMillis();
-                    double val = getRequiredConfig(SIN_AMPLITUDE) * (1 + Math.sin( (1.0*time) / getRequiredConfig(SIN_PERIOD_MS) * Math.PI * 2  - Math.PI/2 )) / 2;
-                    entity.setAttribute(getRequiredConfig(TARGET), val);
-                } catch (Throwable t) {
-                    LOG.warn("Error generating sinusoidal-load metric", t);
-                    throw Throwables.propagate(t);
-                }
-            }
-
-        }, 0, getRequiredConfig(PUBLISH_PERIOD_MS), TimeUnit.MILLISECONDS);
-    }
-    
-    @Override
-    public void destroy() {
-        executor.shutdownNow();
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bd44bb8f/usage/qa/src/test/java/brooklyn/qa/longevity/webcluster/WebClusterApp.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/test/java/brooklyn/qa/longevity/webcluster/WebClusterApp.java b/usage/qa/src/test/java/brooklyn/qa/longevity/webcluster/WebClusterApp.java
deleted file mode 100644
index a223861..0000000
--- a/usage/qa/src/test/java/brooklyn/qa/longevity/webcluster/WebClusterApp.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 brooklyn.qa.longevity.webcluster;
-
-import java.util.List;
-
-import brooklyn.config.BrooklynProperties;
-import brooklyn.enricher.Enrichers;
-import brooklyn.entity.basic.AbstractApplication;
-import brooklyn.entity.basic.Entities;
-import brooklyn.entity.basic.StartableApplication;
-import brooklyn.entity.proxy.nginx.NginxController;
-import brooklyn.entity.proxying.EntitySpec;
-import brooklyn.entity.webapp.ControlledDynamicWebAppCluster;
-import brooklyn.entity.webapp.jboss.JBoss7Server;
-import brooklyn.event.AttributeSensor;
-import brooklyn.event.basic.Sensors;
-import brooklyn.launcher.BrooklynLauncher;
-import brooklyn.policy.EnricherSpec;
-import brooklyn.policy.autoscaling.AutoScalerPolicy;
-import brooklyn.util.CommandLineUtil;
-
-import com.google.common.collect.Lists;
-
-public class WebClusterApp extends AbstractApplication {
-
-    static BrooklynProperties config = BrooklynProperties.Factory.newDefault();
-
-    public static final String WAR_PATH = "classpath://hello-world.war";
-
-    private static final long loadCyclePeriodMs = 2 * 60 * 1000L;
-
-    @Override
-    public void initApp() {
-        final AttributeSensor<Double> sinusoidalLoad =
-                Sensors.newDoubleSensor("brooklyn.qa.sinusoidalLoad", "Sinusoidal server load");
-        AttributeSensor<Double> averageLoad =
-                Sensors.newDoubleSensor("brooklyn.qa.averageLoad", "Average load in cluster");
-
-        NginxController nginxController = addChild(EntitySpec.create(NginxController.class)
-                // .configure("domain", "webclusterexample.brooklyn.local")
-                .configure("port", "8000+"));
-
-        EntitySpec<JBoss7Server> jbossSpec = EntitySpec.create(JBoss7Server.class)
-                .configure("httpPort", "8080+")
-                .configure("war", WAR_PATH)
-                .enricher(EnricherSpec.create(SinusoidalLoadGenerator.class)
-                        .configure(SinusoidalLoadGenerator.TARGET, sinusoidalLoad)
-                        .configure(SinusoidalLoadGenerator.PUBLISH_PERIOD_MS, 500L)
-                        .configure(SinusoidalLoadGenerator.SIN_PERIOD_MS, loadCyclePeriodMs)
-                        .configure(SinusoidalLoadGenerator.SIN_AMPLITUDE, 1d));
-
-        ControlledDynamicWebAppCluster web = addChild(EntitySpec.create(ControlledDynamicWebAppCluster.class)
-                .displayName("WebApp cluster")
-                .configure("controller", nginxController)
-                .configure("initialSize", 1)
-                .configure("memberSpec", jbossSpec));
-
-        web.getCluster().addEnricher(Enrichers.builder()
-                .aggregating(sinusoidalLoad)
-                .publishing(averageLoad)
-                .fromMembers()
-                .computingAverage()
-                .build());
-        web.getCluster().addPolicy(AutoScalerPolicy.builder()
-                .metric(averageLoad)
-                .sizeRange(1, 3)
-                .metricRange(0.3, 0.7)
-                .build());
-    }
-    
-    public static void main(String[] argv) {
-        List<String> args = Lists.newArrayList(argv);
-        String port =  CommandLineUtil.getCommandLineOption(args, "--port", "8081+");
-        String location = CommandLineUtil.getCommandLineOption(args, "--location", "localhost");
-
-        BrooklynLauncher launcher = BrooklynLauncher.newInstance()
-                .application(EntitySpec.create(StartableApplication.class, WebClusterApp.class).displayName("Brooklyn WebApp Cluster example"))
-                .webconsolePort(port)
-                .location(location)
-                .start();
-         
-        Entities.dumpInfo(launcher.getApplications());
-    }
-}