diff --git a/resources/sql/autopatches/20150930.drydock.log.1.sql b/resources/sql/autopatches/20150930.drydock.log.1.sql
new file mode 100644
index 0000000000..e84859b718
--- /dev/null
+++ b/resources/sql/autopatches/20150930.drydock.log.1.sql
@@ -0,0 +1,25 @@
+TRUNCATE {$NAMESPACE}_drydock.drydock_log;
+
+ALTER TABLE {$NAMESPACE}_drydock.drydock_log
+  DROP resourceID;
+
+ALTER TABLE {$NAMESPACE}_drydock.drydock_log
+  DROP leaseID;
+
+ALTER TABLE {$NAMESPACE}_drydock.drydock_log
+  DROP message;
+
+ALTER TABLE {$NAMESPACE}_drydock.drydock_log
+  ADD blueprintPHID VARBINARY(64);
+
+ALTER TABLE {$NAMESPACE}_drydock.drydock_log
+  ADD resourcePHID VARBINARY(64);
+
+ALTER TABLE {$NAMESPACE}_drydock.drydock_log
+  ADD leasePHID VARBINARY(64);
+
+ALTER TABLE {$NAMESPACE}_drydock.drydock_log
+  ADD type VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT};
+
+ALTER TABLE {$NAMESPACE}_drydock.drydock_log
+  ADD data LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/src/applications/drydock/application/PhabricatorDrydockApplication.php b/src/applications/drydock/application/PhabricatorDrydockApplication.php
index 5df54593ee..e662fea9e6 100644
--- a/src/applications/drydock/application/PhabricatorDrydockApplication.php
+++ b/src/applications/drydock/application/PhabricatorDrydockApplication.php
@@ -1,102 +1,105 @@
 <?php
 
 final class PhabricatorDrydockApplication extends PhabricatorApplication {
 
   public function getBaseURI() {
     return '/drydock/';
   }
 
   public function getName() {
     return pht('Drydock');
   }
 
   public function getShortDescription() {
     return pht('Allocate Software Resources');
   }
 
   public function getFontIcon() {
     return 'fa-truck';
   }
 
   public function getTitleGlyph() {
     return "\xE2\x98\x82";
   }
 
   public function getFlavorText() {
     return pht('A nautical adventure.');
   }
 
   public function getApplicationGroup() {
     return self::GROUP_UTILITIES;
   }
 
   public function isPrototype() {
     return true;
   }
 
   public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
     return array(
       array(
         'name' => pht('Drydock User Guide'),
         'href' => PhabricatorEnv::getDoclink('Drydock User Guide'),
       ),
     );
   }
 
   public function getRoutes() {
     return array(
       '/drydock/' => array(
         '' => 'DrydockConsoleController',
-        'blueprint/' => array(
+        '(?P<type>blueprint)/' => array(
           '(?:query/(?P<queryKey>[^/]+)/)?' => 'DrydockBlueprintListController',
           '(?P<id>[1-9]\d*)/' => array(
             '' => 'DrydockBlueprintViewController',
             '(?P<action>disable|enable)/' =>
               'DrydockBlueprintDisableController',
             'resources/(?:query/(?P<queryKey>[^/]+)/)?' =>
               'DrydockResourceListController',
+            'logs/(?:query/(?P<queryKey>[^/]+)/)?' =>
+              'DrydockLogListController',
           ),
           'create/' => 'DrydockBlueprintCreateController',
           'edit/(?:(?P<id>[1-9]\d*)/)?' => 'DrydockBlueprintEditController',
         ),
-        'resource/' => array(
+        '(?P<type>resource)/' => array(
           '(?:query/(?P<queryKey>[^/]+)/)?' => 'DrydockResourceListController',
           '(?P<id>[1-9]\d*)/' => array(
             '' => 'DrydockResourceViewController',
             'release/' => 'DrydockResourceReleaseController',
             'leases/(?:query/(?P<queryKey>[^/]+)/)?' =>
               'DrydockLeaseListController',
+            'logs/(?:query/(?P<queryKey>[^/]+)/)?' =>
+              'DrydockLogListController',
           ),
         ),
-        'lease/' => array(
+        '(?P<type>lease)/' => array(
           '(?:query/(?P<queryKey>[^/]+)/)?' => 'DrydockLeaseListController',
           '(?P<id>[1-9]\d*)/' => array(
             '' => 'DrydockLeaseViewController',
             'release/' => 'DrydockLeaseReleaseController',
+            'logs/(?:query/(?P<queryKey>[^/]+)/)?' =>
+              'DrydockLogListController',
           ),
         ),
-        'log/' => array(
-          '(?:query/(?P<queryKey>[^/]+)/)?' => 'DrydockLogListController',
-        ),
       ),
     );
   }
 
   protected function getCustomCapabilities() {
     return array(
       DrydockDefaultViewCapability::CAPABILITY => array(
         'template' => DrydockBlueprintPHIDType::TYPECONST,
         'capability' => PhabricatorPolicyCapability::CAN_VIEW,
       ),
       DrydockDefaultEditCapability::CAPABILITY => array(
         'default' => PhabricatorPolicies::POLICY_ADMIN,
         'template' => DrydockBlueprintPHIDType::TYPECONST,
         'capability' => PhabricatorPolicyCapability::CAN_EDIT,
       ),
       DrydockCreateBlueprintsCapability::CAPABILITY => array(
         'default' => PhabricatorPolicies::POLICY_ADMIN,
       ),
     );
   }
 
 }
diff --git a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
index f58767c4fc..b7e39a49a4 100644
--- a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
@@ -1,339 +1,299 @@
 <?php
 
 /**
  * @task lease Lease Acquisition
  * @task resource Resource Allocation
  * @task interface Resource Interfaces
  * @task log Logging
  */
 abstract class DrydockBlueprintImplementation extends Phobject {
 
   abstract public function getType();
 
   abstract public function isEnabled();
 
   abstract public function getBlueprintName();
   abstract public function getDescription();
 
   public function getFieldSpecifications() {
     return array();
   }
 
 
 /* -(  Lease Acquisition  )-------------------------------------------------- */
 
 
   /**
    * Enforce basic checks on lease/resource compatibility. Allows resources to
    * reject leases if they are incompatible, even if the resource types match.
    *
    * For example, if a resource represents a 32-bit host, this method might
    * reject leases that need a 64-bit host. The blueprint might also reject
    * a resource if the lease needs 8GB of RAM and the resource only has 6GB
    * free.
    *
    * This method should not acquire locks or expect anything to be locked. This
    * is a coarse compatibility check between a lease and a resource.
    *
    * @param DrydockBlueprint Concrete blueprint to allocate for.
    * @param DrydockResource Candidiate resource to allocate the lease on.
    * @param DrydockLease Pending lease that wants to allocate here.
    * @return bool True if the resource and lease are compatible.
    * @task lease
    */
   abstract public function canAcquireLeaseOnResource(
     DrydockBlueprint $blueprint,
     DrydockResource $resource,
     DrydockLease $lease);
 
 
   /**
    * Acquire a lease. Allows resources to peform setup as leases are brought
    * online.
    *
    * If acquisition fails, throw an exception.
    *
    * @param DrydockBlueprint Blueprint which built the resource.
    * @param DrydockResource Resource to acquire a lease on.
    * @param DrydockLease Requested lease.
    * @return void
    * @task lease
    */
   abstract public function acquireLease(
     DrydockBlueprint $blueprint,
     DrydockResource $resource,
     DrydockLease $lease);
 
 
   /**
    * @return void
    * @task lease
    */
   public function activateLease(
     DrydockBlueprint $blueprint,
     DrydockResource $resource,
     DrydockLease $lease) {
     throw new PhutilMethodNotImplementedException();
   }
 
 
   /**
    * React to a lease being released.
    *
    * This callback is primarily useful for automatically releasing resources
    * once all leases are released.
    *
    * @param DrydockBlueprint Blueprint which built the resource.
    * @param DrydockResource Resource a lease was released on.
    * @param DrydockLease Recently released lease.
    * @return void
    * @task lease
    */
   abstract public function didReleaseLease(
     DrydockBlueprint $blueprint,
     DrydockResource $resource,
     DrydockLease $lease);
 
 
   /**
    * Destroy any temporary data associated with a lease.
    *
    * If a lease creates temporary state while held, destroy it here.
    *
    * @param DrydockBlueprint Blueprint which built the resource.
    * @param DrydockResource Resource the lease is acquired on.
    * @param DrydockLease The lease being destroyed.
    * @return void
    * @task lease
    */
   abstract public function destroyLease(
     DrydockBlueprint $blueprint,
     DrydockResource $resource,
     DrydockLease $lease);
 
 
 /* -(  Resource Allocation  )------------------------------------------------ */
 
 
   /**
    * Enforce fundamental implementation/lease checks. Allows implementations to
    * reject a lease which no concrete blueprint can ever satisfy.
    *
    * For example, if a lease only builds ARM hosts and the lease needs a
    * PowerPC host, it may be rejected here.
    *
    * This is the earliest rejection phase, and followed by
    * @{method:canEverAllocateResourceForLease}.
    *
    * This method should not actually check if a resource can be allocated
    * right now, or even if a blueprint which can allocate a suitable resource
    * really exists, only if some blueprint may conceivably exist which could
    * plausibly be able to build a suitable resource.
    *
    * @param DrydockLease Requested lease.
    * @return bool True if some concrete blueprint of this implementation's
    *   type might ever be able to build a resource for the lease.
    * @task resource
    */
   abstract public function canAnyBlueprintEverAllocateResourceForLease(
     DrydockLease $lease);
 
 
   /**
    * Enforce basic blueprint/lease checks. Allows blueprints to reject a lease
    * which they can not build a resource for.
    *
    * This is the second rejection phase. It follows
    * @{method:canAnyBlueprintEverAllocateResourceForLease} and is followed by
    * @{method:canAllocateResourceForLease}.
    *
    * This method should not check if a resource can be built right now, only
    * if the blueprint as configured may, at some time, be able to build a
    * suitable resource.
    *
    * @param DrydockBlueprint Blueprint which may be asked to allocate a
    *   resource.
    * @param DrydockLease Requested lease.
    * @return bool True if this blueprint can eventually build a suitable
    *   resource for the lease, as currently configured.
    * @task resource
    */
   abstract public function canEverAllocateResourceForLease(
     DrydockBlueprint $blueprint,
     DrydockLease $lease);
 
 
   /**
    * Enforce basic availability limits. Allows blueprints to reject resource
    * allocation if they are currently overallocated.
    *
    * This method should perform basic capacity/limit checks. For example, if
    * it has a limit of 6 resources and currently has 6 resources allocated,
    * it might reject new leases.
    *
    * This method should not acquire locks or expect locks to be acquired. This
    * is a coarse check to determine if the operation is likely to succeed
    * right now without needing to acquire locks.
    *
    * It is expected that this method will sometimes return `true` (indicating
    * that a resource can be allocated) but find that another allocator has
    * eaten up free capacity by the time it actually tries to build a resource.
    * This is normal and the allocator will recover from it.
    *
    * @param DrydockBlueprint The blueprint which may be asked to allocate a
    *   resource.
    * @param DrydockLease Requested lease.
    * @return bool True if this blueprint appears likely to be able to allocate
    *   a suitable resource.
    * @task resource
    */
   abstract public function canAllocateResourceForLease(
     DrydockBlueprint $blueprint,
     DrydockLease $lease);
 
 
   /**
    * Allocate a suitable resource for a lease.
    *
    * This method MUST acquire, hold, and manage locks to prevent multiple
    * allocations from racing. World state is not locked before this method is
    * called. Blueprints are entirely responsible for any lock handling they
    * need to perform.
    *
    * @param DrydockBlueprint The blueprint which should allocate a resource.
    * @param DrydockLease Requested lease.
    * @return DrydockResource Allocated resource.
    * @task resource
    */
   abstract public function allocateResource(
     DrydockBlueprint $blueprint,
     DrydockLease $lease);
 
 
   /**
    * @task resource
    */
   public function activateResource(
     DrydockBlueprint $blueprint,
     DrydockResource $resource) {
     throw new PhutilMethodNotImplementedException();
   }
 
 
   /**
    * Destroy any temporary data associated with a resource.
    *
    * If a resource creates temporary state when allocated, destroy that state
    * here. For example, you might shut down a virtual host or destroy a working
    * copy on disk.
    *
    * @param DrydockBlueprint Blueprint which built the resource.
    * @param DrydockResource Resource being destroyed.
    * @return void
    * @task resource
    */
   abstract public function destroyResource(
     DrydockBlueprint $blueprint,
     DrydockResource $resource);
 
 
 /* -(  Resource Interfaces  )------------------------------------------------ */
 
 
   abstract public function getInterface(
     DrydockBlueprint $blueprint,
     DrydockResource $resource,
     DrydockLease $lease,
     $type);
 
 
 /* -(  Logging  )------------------------------------------------------------ */
 
 
-  /**
-   * @task log
-   */
-  protected function logException(Exception $ex) {
-    $this->log($ex->getMessage());
-  }
-
-
-  /**
-   * @task log
-   */
-  protected function log($message) {
-    self::writeLog(null, null, $message);
-  }
-
-
-  /**
-   * @task log
-   */
-  public static function writeLog(
-    DrydockResource $resource = null,
-    DrydockLease $lease = null,
-    $message = null) {
-
-    $log = id(new DrydockLog())
-      ->setEpoch(time())
-      ->setMessage($message);
-
-    if ($resource) {
-      $log->setResourceID($resource->getID());
-    }
-
-    if ($lease) {
-      $log->setLeaseID($lease->getID());
-    }
-
-    $log->save();
-  }
-
-
   public static function getAllBlueprintImplementations() {
     return id(new PhutilClassMapQuery())
       ->setAncestorClass(__CLASS__)
       ->execute();
   }
 
   public static function getNamedImplementation($class) {
     return idx(self::getAllBlueprintImplementations(), $class);
   }
 
   protected function newResourceTemplate(
     DrydockBlueprint $blueprint,
     $name) {
 
     $resource = id(new DrydockResource())
       ->setBlueprintPHID($blueprint->getPHID())
       ->attachBlueprint($blueprint)
       ->setType($this->getType())
       ->setStatus(DrydockResourceStatus::STATUS_PENDING)
       ->setName($name);
 
     // Pre-allocate the resource PHID.
     $resource->setPHID($resource->generatePHID());
 
     return $resource;
   }
 
   protected function newLease(DrydockBlueprint $blueprint) {
     return id(new DrydockLease());
   }
 
   protected function requireActiveLease(DrydockLease $lease) {
     $lease_status = $lease->getStatus();
 
     switch ($lease_status) {
       case DrydockLeaseStatus::STATUS_ACQUIRED:
         // TODO: Temporary failure.
         throw new Exception(pht('Lease still activating.'));
       case DrydockLeaseStatus::STATUS_ACTIVE:
         return;
       default:
         // TODO: Permanent failure.
         throw new Exception(pht('Lease in bad state.'));
     }
   }
 
 }
diff --git a/src/applications/drydock/controller/DrydockBlueprintViewController.php b/src/applications/drydock/controller/DrydockBlueprintViewController.php
index 6991e18fa2..7102962600 100644
--- a/src/applications/drydock/controller/DrydockBlueprintViewController.php
+++ b/src/applications/drydock/controller/DrydockBlueprintViewController.php
@@ -1,170 +1,178 @@
 <?php
 
 final class DrydockBlueprintViewController extends DrydockBlueprintController {
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $request->getViewer();
     $id = $request->getURIData('id');
 
     $blueprint = id(new DrydockBlueprintQuery())
       ->setViewer($viewer)
       ->withIDs(array($id))
       ->executeOne();
     if (!$blueprint) {
       return new Aphront404Response();
     }
 
     $title = $blueprint->getBlueprintName();
 
     $header = id(new PHUIHeaderView())
       ->setHeader($title)
       ->setUser($viewer)
       ->setPolicyObject($blueprint);
 
     if ($blueprint->getIsDisabled()) {
       $header->setStatus('fa-ban', 'red', pht('Disabled'));
     } else {
       $header->setStatus('fa-check', 'bluegrey', pht('Active'));
     }
 
     $actions = $this->buildActionListView($blueprint);
     $properties = $this->buildPropertyListView($blueprint, $actions);
 
     $crumbs = $this->buildApplicationCrumbs();
     $crumbs->addTextCrumb(pht('Blueprint %d', $blueprint->getID()));
 
     $object_box = id(new PHUIObjectBoxView())
       ->setHeader($header)
       ->addPropertyList($properties);
 
     $field_list = PhabricatorCustomField::getObjectFields(
       $blueprint,
       PhabricatorCustomField::ROLE_VIEW);
     $field_list
       ->setViewer($viewer)
       ->readFieldsFromStorage($blueprint);
 
     $field_list->appendFieldsToPropertyList(
       $blueprint,
       $viewer,
       $properties);
 
     $resource_box = $this->buildResourceBox($blueprint);
 
     $timeline = $this->buildTransactionTimeline(
       $blueprint,
       new DrydockBlueprintTransactionQuery());
     $timeline->setShouldTerminate(true);
 
+    $log_query = id(new DrydockLogQuery())
+      ->withBlueprintPHIDs(array($blueprint->getPHID()));
+
+    $log_box = $this->buildLogBox(
+      $log_query,
+      $this->getApplicationURI("blueprint/{$id}/logs/query/all/"));
+
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $object_box,
         $resource_box,
+        $log_box,
         $timeline,
       ),
       array(
         'title'   => $title,
       ));
 
   }
 
   private function buildActionListView(DrydockBlueprint $blueprint) {
     $viewer = $this->getViewer();
     $id = $blueprint->getID();
 
     $view = id(new PhabricatorActionListView())
       ->setUser($viewer)
       ->setObjectURI($this->getRequest()->getRequestURI())
       ->setObject($blueprint);
 
     $edit_uri = $this->getApplicationURI("blueprint/edit/{$id}/");
 
     $can_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $blueprint,
       PhabricatorPolicyCapability::CAN_EDIT);
 
     $view->addAction(
       id(new PhabricatorActionView())
         ->setHref($edit_uri)
         ->setName(pht('Edit Blueprint'))
         ->setIcon('fa-pencil')
         ->setWorkflow(!$can_edit)
         ->setDisabled(!$can_edit));
 
     if (!$blueprint->getIsDisabled()) {
       $disable_name = pht('Disable Blueprint');
       $disable_icon = 'fa-ban';
       $disable_uri = $this->getApplicationURI("blueprint/{$id}/disable/");
     } else {
       $disable_name = pht('Enable Blueprint');
       $disable_icon = 'fa-check';
       $disable_uri = $this->getApplicationURI("blueprint/{$id}/enable/");
     }
 
     $view->addAction(
       id(new PhabricatorActionView())
         ->setHref($disable_uri)
         ->setName($disable_name)
         ->setIcon($disable_icon)
         ->setWorkflow(true)
         ->setDisabled(!$can_edit));
 
     return $view;
   }
 
   private function buildPropertyListView(
     DrydockBlueprint $blueprint,
     PhabricatorActionListView $actions) {
 
     $view = new PHUIPropertyListView();
     $view->setActionList($actions);
 
     $view->addProperty(
       pht('Type'),
       $blueprint->getImplementation()->getBlueprintName());
 
     return $view;
   }
 
   private function buildResourceBox(DrydockBlueprint $blueprint) {
     $viewer = $this->getViewer();
 
     $resources = id(new DrydockResourceQuery())
       ->setViewer($viewer)
       ->withBlueprintPHIDs(array($blueprint->getPHID()))
       ->withStatuses(
         array(
           DrydockResourceStatus::STATUS_PENDING,
           DrydockResourceStatus::STATUS_ACTIVE,
         ))
       ->setLimit(100)
       ->execute();
 
     $resource_list = id(new DrydockResourceListView())
       ->setUser($viewer)
       ->setResources($resources)
       ->render()
       ->setNoDataString(pht('This blueprint has no active resources.'));
 
     $id = $blueprint->getID();
     $resources_uri = "blueprint/{$id}/resources/query/all/";
     $resources_uri = $this->getApplicationURI($resources_uri);
 
     $resource_header = id(new PHUIHeaderView())
       ->setHeader(pht('Active Resources'))
       ->addActionLink(
         id(new PHUIButtonView())
           ->setTag('a')
           ->setHref($resources_uri)
           ->setIconFont('fa-search')
           ->setText(pht('View All Resources')));
 
     return id(new PHUIObjectBoxView())
       ->setHeader($resource_header)
       ->setObjectList($resource_list);
   }
 
 
 }
diff --git a/src/applications/drydock/controller/DrydockController.php b/src/applications/drydock/controller/DrydockController.php
index e0130bdf56..760334cbdf 100644
--- a/src/applications/drydock/controller/DrydockController.php
+++ b/src/applications/drydock/controller/DrydockController.php
@@ -1,88 +1,115 @@
 <?php
 
 abstract class DrydockController extends PhabricatorController {
 
   abstract public function buildSideNavView();
 
   public function buildApplicationMenu() {
     return $this->buildSideNavView()->getMenu();
   }
 
   protected function buildLocksTab($owner_phid) {
     $locks = DrydockSlotLock::loadLocks($owner_phid);
 
     $rows = array();
     foreach ($locks as $lock) {
       $rows[] = array(
         $lock->getID(),
         $lock->getLockKey(),
       );
     }
 
     $table = id(new AphrontTableView($rows))
       ->setNoDataString(pht('No slot locks held.'))
       ->setHeaders(
         array(
           pht('ID'),
           pht('Lock Key'),
         ))
       ->setColumnClasses(
         array(
           null,
           'wide',
         ));
 
     return id(new PHUIPropertyListView())
       ->addRawContent($table);
   }
 
   protected function buildCommandsTab($target_phid) {
     $viewer = $this->getViewer();
 
     $commands = id(new DrydockCommandQuery())
       ->setViewer($viewer)
       ->withTargetPHIDs(array($target_phid))
       ->execute();
 
     $consumed_yes = id(new PHUIIconView())
       ->setIconFont('fa-check green');
     $consumed_no = id(new PHUIIconView())
       ->setIconFont('fa-clock-o grey');
 
     $rows = array();
     foreach ($commands as $command) {
       $rows[] = array(
         $command->getID(),
         $viewer->renderHandle($command->getAuthorPHID()),
         $command->getCommand(),
         ($command->getIsConsumed()
           ? $consumed_yes
           : $consumed_no),
         phabricator_datetime($command->getDateCreated(), $viewer),
       );
     }
 
     $table = id(new AphrontTableView($rows))
       ->setNoDataString(pht('No commands issued.'))
       ->setHeaders(
         array(
           pht('ID'),
           pht('From'),
           pht('Command'),
           null,
           pht('Date'),
         ))
       ->setColumnClasses(
         array(
           null,
           null,
           'wide',
           null,
           null,
         ));
 
     return id(new PHUIPropertyListView())
       ->addRawContent($table);
   }
 
+  protected function buildLogBox(DrydockLogQuery $query, $all_uri) {
+    $viewer = $this->getViewer();
+
+    $logs = $query
+      ->setViewer($viewer)
+      ->setLimit(100)
+      ->execute();
+
+    $log_table = id(new DrydockLogListView())
+      ->setUser($viewer)
+      ->setLogs($logs)
+      ->render();
+
+    $log_header = id(new PHUIHeaderView())
+      ->setHeader(pht('Logs'))
+      ->addActionLink(
+        id(new PHUIButtonView())
+          ->setTag('a')
+          ->setHref($all_uri)
+          ->setIconFont('fa-search')
+          ->setText(pht('View All Logs')));
+
+    return id(new PHUIObjectBoxView())
+      ->setHeader($log_header)
+      ->setTable($log_table);
+  }
+
 }
diff --git a/src/applications/drydock/controller/DrydockLeaseViewController.php b/src/applications/drydock/controller/DrydockLeaseViewController.php
index af893ca49b..b9cf592313 100644
--- a/src/applications/drydock/controller/DrydockLeaseViewController.php
+++ b/src/applications/drydock/controller/DrydockLeaseViewController.php
@@ -1,158 +1,147 @@
 <?php
 
 final class DrydockLeaseViewController extends DrydockLeaseController {
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $request->getViewer();
     $id = $request->getURIData('id');
 
     $lease = id(new DrydockLeaseQuery())
       ->setViewer($viewer)
       ->withIDs(array($id))
       ->needUnconsumedCommands(true)
       ->executeOne();
     if (!$lease) {
       return new Aphront404Response();
     }
 
-    $lease_uri = $this->getApplicationURI('lease/'.$lease->getID().'/');
+    $id = $lease->getID();
+    $lease_uri = $this->getApplicationURI("lease/{$id}/");
 
     $title = pht('Lease %d', $lease->getID());
 
     $header = id(new PHUIHeaderView())
       ->setHeader($title);
 
     if ($lease->isReleasing()) {
       $header->setStatus('fa-exclamation-triangle', 'red', pht('Releasing'));
     }
 
     $actions = $this->buildActionListView($lease);
     $properties = $this->buildPropertyListView($lease, $actions);
 
-    $pager = new PHUIPagerView();
-    $pager->setURI(new PhutilURI($lease_uri), 'offset');
-    $pager->setOffset($request->getInt('offset'));
+    $log_query = id(new DrydockLogQuery())
+      ->withLeasePHIDs(array($lease->getPHID()));
 
-    $logs = id(new DrydockLogQuery())
-      ->setViewer($viewer)
-      ->withLeaseIDs(array($lease->getID()))
-      ->executeWithOffsetPager($pager);
-
-    $log_table = id(new DrydockLogListView())
-      ->setUser($viewer)
-      ->setLogs($logs)
-      ->render();
-    $log_table->appendChild($pager);
+    $log_box = $this->buildLogBox(
+      $log_query,
+      $this->getApplicationURI("lease/{$id}/logs/query/all/"));
 
     $crumbs = $this->buildApplicationCrumbs();
     $crumbs->addTextCrumb($title, $lease_uri);
 
     $locks = $this->buildLocksTab($lease->getPHID());
     $commands = $this->buildCommandsTab($lease->getPHID());
 
     $object_box = id(new PHUIObjectBoxView())
       ->setHeader($header)
       ->addPropertyList($properties, pht('Properties'))
       ->addPropertyList($locks, pht('Slot Locks'))
       ->addPropertyList($commands, pht('Commands'));
 
-    $log_box = id(new PHUIObjectBoxView())
-      ->setHeaderText(pht('Lease Logs'))
-      ->setTable($log_table);
-
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $object_box,
         $log_box,
       ),
       array(
         'title' => $title,
       ));
 
   }
 
   private function buildActionListView(DrydockLease $lease) {
     $viewer = $this->getViewer();
 
     $view = id(new PhabricatorActionListView())
       ->setUser($viewer)
       ->setObjectURI($this->getRequest()->getRequestURI())
       ->setObject($lease);
 
     $id = $lease->getID();
 
     $can_release = $lease->canRelease();
     if ($lease->isReleasing()) {
       $can_release = false;
     }
 
     $can_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $lease,
       PhabricatorPolicyCapability::CAN_EDIT);
 
     $view->addAction(
       id(new PhabricatorActionView())
         ->setName(pht('Release Lease'))
         ->setIcon('fa-times')
         ->setHref($this->getApplicationURI("/lease/{$id}/release/"))
         ->setWorkflow(true)
         ->setDisabled(!$can_release || !$can_edit));
 
     return $view;
   }
 
   private function buildPropertyListView(
     DrydockLease $lease,
     PhabricatorActionListView $actions) {
     $viewer = $this->getViewer();
 
     $view = new PHUIPropertyListView();
     $view->setActionList($actions);
 
     $view->addProperty(
       pht('Status'),
       DrydockLeaseStatus::getNameForStatus($lease->getStatus()));
 
     $view->addProperty(
       pht('Resource Type'),
       $lease->getResourceType());
 
     $owner_phid = $lease->getOwnerPHID();
     if ($owner_phid) {
       $owner_display = $viewer->renderHandle($owner_phid);
     } else {
       $owner_display = phutil_tag('em', array(), pht('No Owner'));
     }
     $view->addProperty(pht('Owner'), $owner_display);
 
     $resource_phid = $lease->getResourcePHID();
     if ($resource_phid) {
       $resource_display = $viewer->renderHandle($resource_phid);
     } else {
       $resource_display = phutil_tag('em', array(), pht('No Resource'));
     }
     $view->addProperty(pht('Resource'), $resource_display);
 
     $until = $lease->getUntil();
     if ($until) {
       $until_display = phabricator_datetime($until, $viewer);
     } else {
       $until_display = phutil_tag('em', array(), pht('Never'));
     }
     $view->addProperty(pht('Expires'), $until_display);
 
     $attributes = $lease->getAttributes();
     if ($attributes) {
       $view->addSectionHeader(
         pht('Attributes'), 'fa-list-ul');
       foreach ($attributes as $key => $value) {
         $view->addProperty($key, $value);
       }
     }
 
     return $view;
   }
 
 }
diff --git a/src/applications/drydock/controller/DrydockLogController.php b/src/applications/drydock/controller/DrydockLogController.php
index 0e28c62cb4..5ae87e6aad 100644
--- a/src/applications/drydock/controller/DrydockLogController.php
+++ b/src/applications/drydock/controller/DrydockLogController.php
@@ -1,27 +1,119 @@
 <?php
 
 abstract class DrydockLogController
   extends DrydockController {
 
+  private $blueprint;
+  private $resource;
+  private $lease;
+
+  public function setBlueprint(DrydockBlueprint $blueprint) {
+    $this->blueprint = $blueprint;
+    return $this;
+  }
+
+  public function getBlueprint() {
+    return $this->blueprint;
+  }
+
+  public function setResource(DrydockResource $resource) {
+    $this->resource = $resource;
+    return $this;
+  }
+
+  public function getResource() {
+    return $this->resource;
+  }
+
+  public function setLease(DrydockLease $lease) {
+    $this->lease = $lease;
+    return $this;
+  }
+
+  public function getLease() {
+    return $this->lease;
+  }
+
   public function buildSideNavView() {
     $nav = new AphrontSideNavFilterView();
     $nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
 
-    id(new DrydockLogSearchEngine())
-      ->setViewer($this->getRequest()->getUser())
-      ->addNavigationItems($nav->getMenu());
+    $engine = id(new DrydockLogSearchEngine())
+      ->setViewer($this->getRequest()->getUser());
+
+    $blueprint = $this->getBlueprint();
+    if ($blueprint) {
+      $engine->setBlueprint($blueprint);
+    }
+
+    $resource = $this->getResource();
+    if ($resource) {
+      $engine->setResource($resource);
+    }
+
+    $lease = $this->getLease();
+    if ($lease) {
+      $engine->setLease($lease);
+    }
+
+    $engine->addNavigationItems($nav->getMenu());
 
     $nav->selectFilter(null);
 
     return $nav;
   }
 
   protected function buildApplicationCrumbs() {
     $crumbs = parent::buildApplicationCrumbs();
-    $crumbs->addTextCrumb(
-      pht('Logs'),
-      $this->getApplicationURI('log/'));
+
+    $blueprint = $this->getBlueprint();
+    $resource = $this->getResource();
+    $lease = $this->getLease();
+    if ($blueprint) {
+      $id = $blueprint->getID();
+
+      $crumbs->addTextCrumb(
+        pht('Blueprints'),
+        $this->getApplicationURI('blueprint/'));
+
+      $crumbs->addTextCrumb(
+        $blueprint->getBlueprintName(),
+        $this->getApplicationURI("blueprint/{$id}/"));
+
+      $crumbs->addTextCrumb(
+        pht('Logs'),
+        $this->getApplicationURI("blueprint/{$id}/logs/"));
+    } else if ($resource) {
+      $id = $resource->getID();
+
+      $crumbs->addTextCrumb(
+        pht('Resources'),
+        $this->getApplicationURI('resource/'));
+
+      $crumbs->addTextCrumb(
+        $resource->getName(),
+        $this->getApplicationURI("resource/{$id}/"));
+
+      $crumbs->addTextCrumb(
+        pht('Logs'),
+        $this->getApplicationURI("resource/{$id}/logs/"));
+    } else if ($lease) {
+      $id = $lease->getID();
+
+      $crumbs->addTextCrumb(
+        pht('Leases'),
+        $this->getApplicationURI('lease/'));
+
+      $crumbs->addTextCrumb(
+        $lease->getLeaseName(),
+        $this->getApplicationURI("lease/{$id}/"));
+
+      $crumbs->addTextCrumb(
+        pht('Logs'),
+        $this->getApplicationURI("lease/{$id}/logs/"));
+    }
+
     return $crumbs;
   }
 
 }
diff --git a/src/applications/drydock/controller/DrydockLogListController.php b/src/applications/drydock/controller/DrydockLogListController.php
index aecf77dc77..b5e4d4ff0c 100644
--- a/src/applications/drydock/controller/DrydockLogListController.php
+++ b/src/applications/drydock/controller/DrydockLogListController.php
@@ -1,21 +1,63 @@
 <?php
 
 final class DrydockLogListController extends DrydockLogController {
 
   public function shouldAllowPublic() {
     return true;
   }
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $request->getViewer();
-    $querykey = $request->getURIData('queryKey');
+    $engine = new DrydockLogSearchEngine();
+
+    $id = $request->getURIData('id');
+    $type = $request->getURIData('type');
+    switch ($type) {
+      case 'blueprint':
+        $blueprint = id(new DrydockBlueprintQuery())
+          ->setViewer($viewer)
+          ->withIDs(array($id))
+          ->executeOne();
+        if (!$blueprint) {
+          return new Aphront404Response();
+        }
+        $engine->setBlueprint($blueprint);
+        $this->setBlueprint($blueprint);
+        break;
+      case 'resource':
+        $resource = id(new DrydockResourceQuery())
+          ->setViewer($viewer)
+          ->withIDs(array($id))
+          ->executeOne();
+        if (!$resource) {
+          return new Aphront404Response();
+        }
+        $engine->setResource($resource);
+        $this->setResource($resource);
+        break;
+      case 'lease':
+        $lease = id(new DrydockLeaseQuery())
+          ->setViewer($viewer)
+          ->withIDs(array($id))
+          ->executeOne();
+        if (!$lease) {
+          return new Aphront404Response();
+        }
+        $engine->setLease($lease);
+        $this->setLease($lease);
+        break;
+      default:
+        return new Aphront404Response();
+    }
+
+    $query_key = $request->getURIData('queryKey');
 
     $controller = id(new PhabricatorApplicationSearchController())
-      ->setQueryKey($querykey)
-      ->setSearchEngine(new DrydockLogSearchEngine())
+      ->setQueryKey($query_key)
+      ->setSearchEngine($engine)
       ->setNavigation($this->buildSideNavView());
 
     return $this->delegateToController($controller);
   }
 
 }
diff --git a/src/applications/drydock/controller/DrydockResourceViewController.php b/src/applications/drydock/controller/DrydockResourceViewController.php
index 23f81c5225..f97081e673 100644
--- a/src/applications/drydock/controller/DrydockResourceViewController.php
+++ b/src/applications/drydock/controller/DrydockResourceViewController.php
@@ -1,195 +1,183 @@
 <?php
 
 final class DrydockResourceViewController extends DrydockResourceController {
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $request->getViewer();
     $id = $request->getURIData('id');
 
     $resource = id(new DrydockResourceQuery())
       ->setViewer($viewer)
       ->withIDs(array($id))
       ->needUnconsumedCommands(true)
       ->executeOne();
     if (!$resource) {
       return new Aphront404Response();
     }
 
     $title = pht('Resource %s %s', $resource->getID(), $resource->getName());
 
     $header = id(new PHUIHeaderView())
       ->setUser($viewer)
       ->setPolicyObject($resource)
       ->setHeader($title);
 
     if ($resource->isReleasing()) {
       $header->setStatus('fa-exclamation-triangle', 'red', pht('Releasing'));
     }
 
     $actions = $this->buildActionListView($resource);
     $properties = $this->buildPropertyListView($resource, $actions);
 
-    $resource_uri = 'resource/'.$resource->getID().'/';
-    $resource_uri = $this->getApplicationURI($resource_uri);
+    $id = $resource->getID();
+    $resource_uri = $this->getApplicationURI("resource/{$id}/");
 
-    $pager = new PHUIPagerView();
-    $pager->setURI(new PhutilURI($resource_uri), 'offset');
-    $pager->setOffset($request->getInt('offset'));
+    $log_query = id(new DrydockLogQuery())
+      ->withResourcePHIDs(array($resource->getPHID()));
 
-    $logs = id(new DrydockLogQuery())
-      ->setViewer($viewer)
-      ->withResourceIDs(array($resource->getID()))
-      ->executeWithOffsetPager($pager);
-
-    $log_table = id(new DrydockLogListView())
-      ->setUser($viewer)
-      ->setLogs($logs)
-      ->render();
-    $log_table->appendChild($pager);
+    $log_box = $this->buildLogBox(
+      $log_query,
+      $this->getApplicationURI("resource/{$id}/logs/query/all/"));
 
     $crumbs = $this->buildApplicationCrumbs();
     $crumbs->addTextCrumb(pht('Resource %d', $resource->getID()));
 
     $locks = $this->buildLocksTab($resource->getPHID());
     $commands = $this->buildCommandsTab($resource->getPHID());
 
     $object_box = id(new PHUIObjectBoxView())
       ->setHeader($header)
       ->addPropertyList($properties, pht('Properties'))
       ->addPropertyList($locks, pht('Slot Locks'))
       ->addPropertyList($commands, pht('Commands'));
 
     $lease_box = $this->buildLeaseBox($resource);
 
-    $log_box = id(new PHUIObjectBoxView())
-      ->setHeaderText(pht('Resource Logs'))
-      ->setTable($log_table);
-
     return $this->buildApplicationPage(
       array(
         $crumbs,
         $object_box,
         $lease_box,
         $log_box,
       ),
       array(
         'title'   => $title,
       ));
 
   }
 
   private function buildActionListView(DrydockResource $resource) {
     $viewer = $this->getViewer();
 
     $view = id(new PhabricatorActionListView())
       ->setUser($viewer)
       ->setObjectURI($this->getRequest()->getRequestURI())
       ->setObject($resource);
 
     $can_release = $resource->canRelease();
     if ($resource->isReleasing()) {
       $can_release = false;
     }
 
     $can_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $resource,
       PhabricatorPolicyCapability::CAN_EDIT);
 
     $uri = '/resource/'.$resource->getID().'/release/';
     $uri = $this->getApplicationURI($uri);
 
     $view->addAction(
       id(new PhabricatorActionView())
         ->setHref($uri)
         ->setName(pht('Release Resource'))
         ->setIcon('fa-times')
         ->setWorkflow(true)
         ->setDisabled(!$can_release || !$can_edit));
 
     return $view;
   }
 
   private function buildPropertyListView(
     DrydockResource $resource,
     PhabricatorActionListView $actions) {
     $viewer = $this->getViewer();
 
     $view = id(new PHUIPropertyListView())
       ->setActionList($actions);
 
     $status = $resource->getStatus();
     $status = DrydockResourceStatus::getNameForStatus($status);
 
     $view->addProperty(
       pht('Status'),
       $status);
 
     $until = $resource->getUntil();
     if ($until) {
       $until_display = phabricator_datetime($until, $viewer);
     } else {
       $until_display = phutil_tag('em', array(), pht('Never'));
     }
     $view->addProperty(pht('Expires'), $until_display);
 
     $view->addProperty(
       pht('Resource Type'),
       $resource->getType());
 
     $view->addProperty(
       pht('Blueprint'),
       $viewer->renderHandle($resource->getBlueprintPHID()));
 
     $attributes = $resource->getAttributes();
     if ($attributes) {
       $view->addSectionHeader(
         pht('Attributes'), 'fa-list-ul');
       foreach ($attributes as $key => $value) {
         $view->addProperty($key, $value);
       }
     }
 
     return $view;
   }
 
   private function buildLeaseBox(DrydockResource $resource) {
     $viewer = $this->getViewer();
 
     $leases = id(new DrydockLeaseQuery())
       ->setViewer($viewer)
       ->withResourcePHIDs(array($resource->getPHID()))
       ->withStatuses(
         array(
           DrydockLeaseStatus::STATUS_PENDING,
           DrydockLeaseStatus::STATUS_ACQUIRED,
           DrydockLeaseStatus::STATUS_ACTIVE,
         ))
       ->setLimit(100)
       ->execute();
 
     $id = $resource->getID();
     $leases_uri = "resource/{$id}/leases/query/all/";
     $leases_uri = $this->getApplicationURI($leases_uri);
 
     $lease_header = id(new PHUIHeaderView())
       ->setHeader(pht('Active Leases'))
       ->addActionLink(
         id(new PHUIButtonView())
           ->setTag('a')
           ->setHref($leases_uri)
           ->setIconFont('fa-search')
           ->setText(pht('View All Leases')));
 
     $lease_list = id(new DrydockLeaseListView())
       ->setUser($viewer)
       ->setLeases($leases)
       ->render()
       ->setNoDataString(pht('This resource has no active leases.'));
 
     return id(new PHUIObjectBoxView())
       ->setHeader($lease_header)
       ->setObjectList($lease_list);
   }
 
 }
diff --git a/src/applications/drydock/query/DrydockLogQuery.php b/src/applications/drydock/query/DrydockLogQuery.php
index 47a6795463..00980edb4d 100644
--- a/src/applications/drydock/query/DrydockLogQuery.php
+++ b/src/applications/drydock/query/DrydockLogQuery.php
@@ -1,113 +1,126 @@
 <?php
 
 final class DrydockLogQuery extends DrydockQuery {
 
-  private $resourceIDs;
-  private $leaseIDs;
+  private $blueprintPHIDs;
+  private $resourcePHIDs;
+  private $leasePHIDs;
 
-  public function withResourceIDs(array $ids) {
-    $this->resourceIDs = $ids;
+  public function withBlueprintPHIDs(array $phids) {
+    $this->blueprintPHIDs = $phids;
     return $this;
   }
 
-  public function withLeaseIDs(array $ids) {
-    $this->leaseIDs = $ids;
+  public function withResourcePHIDs(array $phids) {
+    $this->resourcePHIDs = $phids;
     return $this;
   }
 
+  public function withLeasePHIDs(array $phids) {
+    $this->leasePHIDs = $phids;
+    return $this;
+  }
+
+  public function newResultObject() {
+    return new DrydockLog();
+  }
+
   protected function loadPage() {
-    $table = new DrydockLog();
-    $conn_r = $table->establishConnection('r');
-
-    $data = queryfx_all(
-      $conn_r,
-      'SELECT log.* FROM %T log %Q %Q %Q',
-      $table->getTableName(),
-      $this->buildWhereClause($conn_r),
-      $this->buildOrderClause($conn_r),
-      $this->buildLimitClause($conn_r));
-
-    return $table->loadAllFromArray($data);
+    return $this->loadStandardPage($this->newResultObject());
   }
 
-  protected function willFilterPage(array $logs) {
-    $resource_ids = array_filter(mpull($logs, 'getResourceID'));
-    if ($resource_ids) {
+  protected function didFilterPage(array $logs) {
+    $blueprint_phids = array_filter(mpull($logs, 'getBlueprintPHID'));
+    if ($blueprint_phids) {
+      $blueprints = id(new DrydockBlueprintQuery())
+        ->setParentQuery($this)
+        ->setViewer($this->getViewer())
+        ->withPHIDs($blueprint_phids)
+        ->execute();
+      $blueprints = mpull($blueprints, null, 'getPHID');
+    } else {
+      $blueprints = array();
+    }
+
+    foreach ($logs as $key => $log) {
+      $blueprint = null;
+      $blueprint_phid = $log->getBlueprintPHID();
+      if ($blueprint_phid) {
+        $blueprint = idx($blueprints, $blueprint_phid);
+      }
+      $log->attachBlueprint($blueprint);
+    }
+
+    $resource_phids = array_filter(mpull($logs, 'getResourcePHID'));
+    if ($resource_phids) {
       $resources = id(new DrydockResourceQuery())
         ->setParentQuery($this)
         ->setViewer($this->getViewer())
-        ->withIDs(array_unique($resource_ids))
+        ->withPHIDs($resource_phids)
         ->execute();
+      $resources = mpull($resources, null, 'getPHID');
     } else {
       $resources = array();
     }
 
     foreach ($logs as $key => $log) {
       $resource = null;
-      if ($log->getResourceID()) {
-        $resource = idx($resources, $log->getResourceID());
-        if (!$resource) {
-          unset($logs[$key]);
-          continue;
-        }
+      $resource_phid = $log->getResourcePHID();
+      if ($resource_phid) {
+        $resource = idx($resources, $resource_phid);
       }
       $log->attachResource($resource);
     }
 
-    $lease_ids = array_filter(mpull($logs, 'getLeaseID'));
-    if ($lease_ids) {
+    $lease_phids = array_filter(mpull($logs, 'getLeasePHID'));
+    if ($lease_phids) {
       $leases = id(new DrydockLeaseQuery())
         ->setParentQuery($this)
         ->setViewer($this->getViewer())
-        ->withIDs(array_unique($lease_ids))
+        ->withPHIDs($lease_phids)
         ->execute();
+      $leases = mpull($leases, null, 'getPHID');
     } else {
       $leases = array();
     }
 
     foreach ($logs as $key => $log) {
       $lease = null;
-      if ($log->getLeaseID()) {
-        $lease = idx($leases, $log->getLeaseID());
-        if (!$lease) {
-          unset($logs[$key]);
-          continue;
-        }
+      $lease_phid = $log->getLeasePHID();
+      if ($lease_phid) {
+        $lease = idx($leases, $lease_phid);
       }
       $log->attachLease($lease);
     }
 
-    // These logs are meaningless and their policies aren't computable. They
-    // shouldn't exist, but throw them away if they do.
-    foreach ($logs as $key => $log) {
-      if (!$log->getResource() && !$log->getLease()) {
-        unset($logs[$key]);
-      }
-    }
-
     return $logs;
   }
 
-  protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
-    $where = array();
+  protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+    $where = parent::buildWhereClauseParts($conn);
 
-    if ($this->resourceIDs !== null) {
+    if ($this->blueprintPHIDs !== null) {
       $where[] = qsprintf(
-        $conn_r,
-        'resourceID IN (%Ld)',
-        $this->resourceIDs);
+        $conn,
+        'blueprintPHID IN (%Ls)',
+        $this->blueprintPHIDs);
     }
 
-    if ($this->leaseIDs !== null) {
+    if ($this->resourcePHIDs !== null) {
       $where[] = qsprintf(
-        $conn_r,
-        'leaseID IN (%Ld)',
-        $this->leaseIDs);
+        $conn,
+        'resourcePHID IN (%Ls)',
+        $this->resourcePHIDs);
     }
 
-    $where[] = $this->buildPagingClause($conn_r);
+    if ($this->leasePHIDs !== null) {
+      $where[] = qsprintf(
+        $conn,
+        'leasePHID IN (%Ls)',
+        $this->leasePHIDs);
+    }
 
-    return $this->formatWhereClause($where);
+    return $where;
   }
 
 }
diff --git a/src/applications/drydock/query/DrydockLogSearchEngine.php b/src/applications/drydock/query/DrydockLogSearchEngine.php
index 13777031d6..43b1511c01 100644
--- a/src/applications/drydock/query/DrydockLogSearchEngine.php
+++ b/src/applications/drydock/query/DrydockLogSearchEngine.php
@@ -1,117 +1,138 @@
 <?php
 
 final class DrydockLogSearchEngine extends PhabricatorApplicationSearchEngine {
 
-  public function getResultTypeDescription() {
-    return pht('Drydock Logs');
+  private $blueprint;
+  private $resource;
+  private $lease;
+
+  public function setBlueprint(DrydockBlueprint $blueprint) {
+    $this->blueprint = $blueprint;
+    return $this;
   }
 
-  public function getApplicationClassName() {
-    return 'PhabricatorDrydockApplication';
+  public function getBlueprint() {
+    return $this->blueprint;
   }
 
-  public function buildSavedQueryFromRequest(AphrontRequest $request) {
-    $query = new PhabricatorSavedQuery();
+  public function setResource(DrydockResource $resource) {
+    $this->resource = $resource;
+    return $this;
+  }
 
-    $query->setParameter(
-      'resourcePHIDs',
-      $this->readListFromRequest($request, 'resources'));
-    $query->setParameter(
-      'leasePHIDs',
-      $this->readListFromRequest($request, 'leases'));
+  public function getResource() {
+    return $this->resource;
+  }
 
-    return $query;
+  public function setLease(DrydockLease $lease) {
+    $this->lease = $lease;
+    return $this;
   }
 
-  public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
-    $resource_phids = $saved->getParameter('resourcePHIDs', array());
-    $lease_phids = $saved->getParameter('leasePHIDs', array());
+  public function getLease() {
+    return $this->lease;
+  }
 
-     // TODO: Change logs to use PHIDs instead of IDs.
-    $resource_ids = array();
-    $lease_ids = array();
+  public function canUseInPanelContext() {
+    // Prevent use on Dashboard panels since all log queries currently need a
+    // parent object and these don't seem particularly useful in any case.
+    return false;
+  }
 
-    if ($resource_phids) {
-      $resource_ids = id(new DrydockResourceQuery())
-        ->setViewer(PhabricatorUser::getOmnipotentUser())
-        ->withPHIDs($resource_phids)
-        ->execute();
-      $resource_ids = mpull($resource_ids, 'getID');
-    }
+  public function getResultTypeDescription() {
+    return pht('Drydock Logs');
+  }
 
-    if ($lease_phids) {
-      $lease_ids = id(new DrydockLeaseQuery())
-        ->setViewer(PhabricatorUser::getOmnipotentUser())
-        ->withPHIDs($lease_phids)
-        ->execute();
-      $lease_ids = mpull($lease_ids, 'getID');
-    }
+  public function getApplicationClassName() {
+    return 'PhabricatorDrydockApplication';
+  }
 
+  public function newQuery() {
     $query = new DrydockLogQuery();
-    if ($resource_ids) {
-      $query->withResourceIDs($resource_ids);
+
+    $blueprint = $this->getBlueprint();
+    if ($blueprint) {
+      $query->withBlueprintPHIDs(array($blueprint->getPHID()));
+    }
+
+    $resource = $this->getResource();
+    if ($resource) {
+      $query->withResourcePHIDs(array($resource->getPHID()));
     }
-    if ($lease_ids) {
-      $query->withLeaseIDs($lease_ids);
+
+    $lease = $this->getLease();
+    if ($lease) {
+      $query->withLeasePHIDs(array($lease->getPHID()));
     }
 
     return $query;
   }
 
-  public function buildSearchForm(
-    AphrontFormView $form,
-    PhabricatorSavedQuery $saved) {
+  protected function buildQueryFromParameters(array $map) {
+    $query = $this->newQuery();
 
-    $form
-      ->appendControl(
-        id(new AphrontFormTokenizerControl())
-          ->setDatasource(new DrydockResourceDatasource())
-          ->setName('resources')
-          ->setLabel(pht('Resources'))
-          ->setValue($saved->getParameter('resourcePHIDs', array())))
-      ->appendControl(
-        id(new AphrontFormTokenizerControl())
-          ->setDatasource(new DrydockLeaseDatasource())
-          ->setName('leases')
-          ->setLabel(pht('Leases'))
-          ->setValue($saved->getParameter('leasePHIDs', array())));
+    return $query;
+  }
+
+  protected function buildCustomSearchFields() {
+    return array();
   }
 
   protected function getURI($path) {
-    return '/drydock/log/'.$path;
+    $blueprint = $this->getBlueprint();
+    if ($blueprint) {
+      $id = $blueprint->getID();
+      return "/drydock/blueprint/{$id}/logs/{$path}";
+    }
+
+    $resource = $this->getResource();
+    if ($resource) {
+      $id = $resource->getID();
+      return "/drydock/resource/{$id}/logs/{$path}";
+    }
+
+    $lease = $this->getLease();
+    if ($lease) {
+      $id = $lease->getID();
+      return "/drydock/lease/{$id}/logs/{$path}";
+    }
+
+    throw new Exception(
+      pht(
+        'Search engine has no blueprint, resource, or lease.'));
   }
 
   protected function getBuiltinQueryNames() {
     return array(
       'all' => pht('All Logs'),
     );
   }
 
   public function buildSavedQueryFromBuiltin($query_key) {
     $query = $this->newSavedQuery();
     $query->setQueryKey($query_key);
 
     switch ($query_key) {
       case 'all':
         return $query;
     }
 
     return parent::buildSavedQueryFromBuiltin($query_key);
   }
 
   protected function renderResultList(
     array $logs,
     PhabricatorSavedQuery $query,
     array $handles) {
 
     $list = id(new DrydockLogListView())
       ->setUser($this->requireViewer())
       ->setLogs($logs);
 
     $result = new PhabricatorApplicationSearchResultView();
     $result->setTable($list);
 
     return $result;
   }
 
 }
diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php
index af0b322b62..b16efbabbb 100644
--- a/src/applications/drydock/storage/DrydockLease.php
+++ b/src/applications/drydock/storage/DrydockLease.php
@@ -1,406 +1,427 @@
 <?php
 
 final class DrydockLease extends DrydockDAO
   implements PhabricatorPolicyInterface {
 
   protected $resourcePHID;
   protected $resourceType;
   protected $until;
   protected $ownerPHID;
   protected $attributes = array();
   protected $status = DrydockLeaseStatus::STATUS_PENDING;
 
   private $resource = self::ATTACHABLE;
   private $unconsumedCommands = self::ATTACHABLE;
 
   private $releaseOnDestruction;
   private $isAcquired = false;
   private $isActivated = false;
   private $activateWhenAcquired = false;
   private $slotLocks = array();
 
   /**
    * Flag this lease to be released when its destructor is called. This is
    * mostly useful if you have a script which acquires, uses, and then releases
    * a lease, as you don't need to explicitly handle exceptions to properly
    * release the lease.
    */
   public function releaseOnDestruction() {
     $this->releaseOnDestruction = true;
     return $this;
   }
 
   public function __destruct() {
     if (!$this->releaseOnDestruction) {
       return;
     }
 
     if (!$this->canRelease()) {
       return;
     }
 
     $actor = PhabricatorUser::getOmnipotentUser();
     $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID();
 
     $command = DrydockCommand::initializeNewCommand($actor)
       ->setTargetPHID($this->getPHID())
       ->setAuthorPHID($drydock_phid)
       ->setCommand(DrydockCommand::COMMAND_RELEASE)
       ->save();
 
     $this->scheduleUpdate();
   }
 
   public function getLeaseName() {
     return pht('Lease %d', $this->getID());
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_SERIALIZATION => array(
         'attributes'    => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_COLUMN_SCHEMA => array(
         'status' => 'text32',
         'until' => 'epoch?',
         'resourceType' => 'text128',
         'ownerPHID' => 'phid?',
         'resourcePHID' => 'phid?',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_resource' => array(
           'columns' => array('resourcePHID', 'status'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function setAttribute($key, $value) {
     $this->attributes[$key] = $value;
     return $this;
   }
 
   public function getAttribute($key, $default = null) {
     return idx($this->attributes, $key, $default);
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(DrydockLeasePHIDType::TYPECONST);
   }
 
   public function getInterface($type) {
     return $this->getResource()->getInterface($this, $type);
   }
 
   public function getResource() {
     return $this->assertAttached($this->resource);
   }
 
   public function attachResource(DrydockResource $resource = null) {
     $this->resource = $resource;
     return $this;
   }
 
   public function hasAttachedResource() {
     return ($this->resource !== null);
   }
 
   public function getUnconsumedCommands() {
     return $this->assertAttached($this->unconsumedCommands);
   }
 
   public function attachUnconsumedCommands(array $commands) {
     $this->unconsumedCommands = $commands;
     return $this;
   }
 
   public function isReleasing() {
     foreach ($this->getUnconsumedCommands() as $command) {
       if ($command->getCommand() == DrydockCommand::COMMAND_RELEASE) {
         return true;
       }
     }
 
     return false;
   }
 
   public function queueForActivation() {
     if ($this->getID()) {
       throw new Exception(
         pht('Only new leases may be queued for activation!'));
     }
 
     $this
       ->setStatus(DrydockLeaseStatus::STATUS_PENDING)
       ->save();
 
     $task = PhabricatorWorker::scheduleTask(
       'DrydockAllocatorWorker',
       array(
         'leasePHID' => $this->getPHID(),
       ),
       array(
         'objectPHID' => $this->getPHID(),
       ));
 
     return $this;
   }
 
   public function isActivating() {
     switch ($this->getStatus()) {
       case DrydockLeaseStatus::STATUS_PENDING:
       case DrydockLeaseStatus::STATUS_ACQUIRED:
         return true;
     }
 
     return false;
   }
 
   public function isActive() {
     switch ($this->getStatus()) {
       case DrydockLeaseStatus::STATUS_ACTIVE:
         return true;
     }
 
     return false;
   }
 
   public function waitUntilActive() {
     while (true) {
       $lease = $this->reload();
       if (!$lease) {
         throw new Exception(pht('Failed to reload lease.'));
       }
 
       $status = $lease->getStatus();
 
       switch ($status) {
         case DrydockLeaseStatus::STATUS_ACTIVE:
           return;
         case DrydockLeaseStatus::STATUS_RELEASED:
           throw new Exception(pht('Lease has already been released!'));
         case DrydockLeaseStatus::STATUS_DESTROYED:
           throw new Exception(pht('Lease has already been destroyed!'));
         case DrydockLeaseStatus::STATUS_BROKEN:
           throw new Exception(pht('Lease has been broken!'));
         case DrydockLeaseStatus::STATUS_PENDING:
         case DrydockLeaseStatus::STATUS_ACQUIRED:
           break;
         default:
           throw new Exception(
             pht(
               'Lease has unknown status "%s".',
               $status));
       }
 
       sleep(1);
     }
   }
 
   public function setActivateWhenAcquired($activate) {
     $this->activateWhenAcquired = true;
     return $this;
   }
 
   public function needSlotLock($key) {
     $this->slotLocks[] = $key;
     return $this;
   }
 
   public function acquireOnResource(DrydockResource $resource) {
     $expect_status = DrydockLeaseStatus::STATUS_PENDING;
     $actual_status = $this->getStatus();
     if ($actual_status != $expect_status) {
       throw new Exception(
         pht(
           'Trying to acquire a lease on a resource which is in the wrong '.
           'state: status must be "%s", actually "%s".',
           $expect_status,
           $actual_status));
     }
 
     if ($this->activateWhenAcquired) {
       $new_status = DrydockLeaseStatus::STATUS_ACTIVE;
     } else {
       $new_status = DrydockLeaseStatus::STATUS_ACQUIRED;
     }
 
     if ($new_status == DrydockLeaseStatus::STATUS_ACTIVE) {
       if ($resource->getStatus() == DrydockResourceStatus::STATUS_PENDING) {
         throw new Exception(
           pht(
             'Trying to acquire an active lease on a pending resource. '.
             'You can not immediately activate leases on resources which '.
             'need time to start up.'));
       }
     }
 
     $this->openTransaction();
 
       $this
         ->setResourcePHID($resource->getPHID())
         ->setStatus($new_status)
         ->save();
 
       DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks);
       $this->slotLocks = array();
 
     $this->saveTransaction();
 
     $this->isAcquired = true;
 
     if ($new_status == DrydockLeaseStatus::STATUS_ACTIVE) {
       $this->didActivate();
     }
 
     return $this;
   }
 
   public function isAcquiredLease() {
     return $this->isAcquired;
   }
 
   public function activateOnResource(DrydockResource $resource) {
     $expect_status = DrydockLeaseStatus::STATUS_ACQUIRED;
     $actual_status = $this->getStatus();
     if ($actual_status != $expect_status) {
       throw new Exception(
         pht(
           'Trying to activate a lease which has the wrong status: status '.
           'must be "%s", actually "%s".',
           $expect_status,
           $actual_status));
     }
 
     if ($resource->getStatus() == DrydockResourceStatus::STATUS_PENDING) {
       // TODO: Be stricter about this?
       throw new Exception(
         pht(
           'Trying to activate a lease on a pending resource.'));
     }
 
     $this->openTransaction();
 
       $this
         ->setStatus(DrydockLeaseStatus::STATUS_ACTIVE)
         ->save();
 
       DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks);
       $this->slotLocks = array();
 
     $this->saveTransaction();
 
     $this->isActivated = true;
 
     $this->didActivate();
 
     return $this;
   }
 
   public function isActivatedLease() {
     return $this->isActivated;
   }
 
   public function canRelease() {
     if (!$this->getID()) {
       return false;
     }
 
     switch ($this->getStatus()) {
       case DrydockLeaseStatus::STATUS_RELEASED:
       case DrydockLeaseStatus::STATUS_DESTROYED:
         return false;
       default:
         return true;
     }
   }
 
   public function canUpdate() {
     switch ($this->getStatus()) {
       case DrydockLeaseStatus::STATUS_ACTIVE:
         return true;
       default:
         return false;
     }
   }
 
   public function scheduleUpdate($epoch = null) {
     PhabricatorWorker::scheduleTask(
       'DrydockLeaseUpdateWorker',
       array(
         'leasePHID' => $this->getPHID(),
         'isExpireTask' => ($epoch !== null),
       ),
       array(
         'objectPHID' => $this->getPHID(),
         'delayUntil' => ($epoch ? (int)$epoch : null),
       ));
   }
 
   public function setAwakenTaskIDs(array $ids) {
     $this->setAttribute('internal.awakenTaskIDs', $ids);
     return $this;
   }
 
   private function didActivate() {
     $viewer = PhabricatorUser::getOmnipotentUser();
     $need_update = false;
 
+    // TODO: This is just a placeholder to get some data in the table.
+    $this->logEvent('activated');
+
     $commands = id(new DrydockCommandQuery())
       ->setViewer($viewer)
       ->withTargetPHIDs(array($this->getPHID()))
       ->withConsumed(false)
       ->execute();
     if ($commands) {
       $need_update = true;
     }
 
     if ($need_update) {
       $this->scheduleUpdate();
     }
 
     $expires = $this->getUntil();
     if ($expires) {
       $this->scheduleUpdate($expires);
     }
 
     $awaken_ids = $this->getAttribute('internal.awakenTaskIDs');
     if (is_array($awaken_ids) && $awaken_ids) {
       PhabricatorWorker::awakenTaskIDs($awaken_ids);
     }
   }
 
+  public function logEvent($type, array $data = array()) {
+    $log = id(new DrydockLog())
+      ->setEpoch(PhabricatorTime::getNow())
+      ->setType($type)
+      ->setData($data);
+
+    $log->setLeasePHID($this->getPHID());
+
+    $resource = $this->getResource();
+    if ($resource) {
+      $log->setResourcePHID($resource->getPHID());
+      $log->setBlueprintPHID($resource->getBlueprintPHID());
+    }
+
+    return $log->save();
+  }
+
+
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     if ($this->getResource()) {
       return $this->getResource()->getPolicy($capability);
     }
 
     // TODO: Implement reasonable policies.
 
     return PhabricatorPolicies::getMostOpenPolicy();
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     if ($this->getResource()) {
       return $this->getResource()->hasAutomaticCapability($capability, $viewer);
     }
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     return pht('Leases inherit policies from the resources they lease.');
   }
 
 }
diff --git a/src/applications/drydock/storage/DrydockLog.php b/src/applications/drydock/storage/DrydockLog.php
index 36d310c510..5d75d82c65 100644
--- a/src/applications/drydock/storage/DrydockLog.php
+++ b/src/applications/drydock/storage/DrydockLog.php
@@ -1,82 +1,115 @@
 <?php
 
 final class DrydockLog extends DrydockDAO
   implements PhabricatorPolicyInterface {
 
-  protected $resourceID;
-  protected $leaseID;
+  protected $blueprintPHID;
+  protected $resourcePHID;
+  protected $leasePHID;
   protected $epoch;
-  protected $message;
+  protected $type;
+  protected $data = array();
 
+  private $blueprint = self::ATTACHABLE;
   private $resource = self::ATTACHABLE;
   private $lease = self::ATTACHABLE;
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_TIMESTAMPS => false,
+      self::CONFIG_SERIALIZATION => array(
+        'data' => self::SERIALIZATION_JSON,
+      ),
       self::CONFIG_COLUMN_SCHEMA => array(
-        'resourceID' => 'id?',
-        'leaseID' => 'id?',
-        'message' => 'text',
+        'blueprintPHID' => 'phid?',
+        'resourcePHID' => 'phid?',
+        'leasePHID' => 'phid?',
+        'type' => 'text64',
       ),
       self::CONFIG_KEY_SCHEMA => array(
-        'resourceID' => array(
-          'columns' => array('resourceID', 'epoch'),
+        'key_blueprint' => array(
+          'columns' => array('blueprintPHID', 'type'),
+        ),
+        'key_resource' => array(
+          'columns' => array('resourcePHID', 'type'),
         ),
-        'leaseID' => array(
-          'columns' => array('leaseID', 'epoch'),
+        'key_lease' => array(
+          'columns' => array('leasePHID', 'type'),
         ),
         'epoch' => array(
           'columns' => array('epoch'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
+  public function attachBlueprint(DrydockBlueprint $blueprint = null) {
+    $this->blueprint = $blueprint;
+    return $this;
+  }
+
+  public function getBlueprint() {
+    return $this->assertAttached($this->blueprint);
+  }
+
   public function attachResource(DrydockResource $resource = null) {
     $this->resource = $resource;
     return $this;
   }
 
   public function getResource() {
     return $this->assertAttached($this->resource);
   }
 
   public function attachLease(DrydockLease $lease = null) {
     $this->lease = $lease;
     return $this;
   }
 
   public function getLease() {
     return $this->assertAttached($this->lease);
   }
 
+  public function isComplete() {
+    if ($this->getBlueprintPHID() && !$this->getBlueprint()) {
+      return false;
+    }
+
+    if ($this->getResourcePHID() && !$this->getResource()) {
+      return false;
+    }
+
+    if ($this->getLeasePHID() && !$this->getLease()) {
+      return false;
+    }
+
+    return true;
+  }
+
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
   public function getPolicy($capability) {
-    if ($this->getResource()) {
-      return $this->getResource()->getPolicy($capability);
-    }
-    return $this->getLease()->getPolicy($capability);
+    // NOTE: We let you see that logs exist no matter what, but don't actually
+    // show you log content unless you can see all of the associated objects.
+    return PhabricatorPolicies::getMostOpenPolicy();
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
-    if ($this->getResource()) {
-      return $this->getResource()->hasAutomaticCapability($capability, $viewer);
-    }
-    return $this->getLease()->hasAutomaticCapability($capability, $viewer);
+    return false;
   }
 
   public function describeAutomaticCapability($capability) {
-    return pht('Logs inherit the policy of their resources.');
+    return pht(
+      'To view log details, you must be able to view the associated '.
+      'blueprint, resource and lease.');
   }
 
 }
diff --git a/src/applications/drydock/view/DrydockLogListView.php b/src/applications/drydock/view/DrydockLogListView.php
index 22e280939b..f6560270a0 100644
--- a/src/applications/drydock/view/DrydockLogListView.php
+++ b/src/applications/drydock/view/DrydockLogListView.php
@@ -1,74 +1,88 @@
 <?php
 
 final class DrydockLogListView extends AphrontView {
 
   private $logs;
 
   public function setLogs(array $logs) {
     assert_instances_of($logs, 'DrydockLog');
     $this->logs = $logs;
     return $this;
   }
 
   public function render() {
     $logs = $this->logs;
     $viewer = $this->getUser();
 
     $view = new PHUIObjectItemListView();
 
     $rows = array();
     foreach ($logs as $log) {
-      $resource_uri = '/drydock/resource/'.$log->getResourceID().'/';
-      $lease_uri = '/drydock/lease/'.$log->getLeaseID().'/';
+      $blueprint_phid = $log->getBlueprintPHID();
+      if ($blueprint_phid) {
+        $blueprint = $viewer->renderHandle($blueprint_phid);
+      } else {
+        $blueprint = null;
+      }
+
+      $resource_phid = $log->getResourcePHID();
+      if ($resource_phid) {
+        $resource = $viewer->renderHandle($resource_phid);
+      } else {
+        $resource = null;
+      }
+
+      $lease_phid = $log->getLeasePHID();
+      if ($lease_phid) {
+        $lease = $viewer->renderHandle($lease_phid);
+      } else {
+        $lease = null;
+      }
 
-      $resource_name = $log->getResourceID();
-      if ($log->getResourceID() !== null) {
-        $resource_name = $log->getResource()->getName();
+      if ($log->isComplete()) {
+        // TODO: This is a placeholder.
+        $type = $log->getType();
+        $data = print_r($log->getData(), true);
+      } else {
+        $type = phutil_tag('em', array(), pht('Restricted'));
+        $data = phutil_tag(
+          'em',
+          array(),
+          pht('You do not have permission to view this log event.'));
       }
 
       $rows[] = array(
-        phutil_tag(
-          'a',
-          array(
-            'href' => $resource_uri,
-          ),
-          $resource_name),
-        phutil_tag(
-          'a',
-          array(
-            'href' => $lease_uri,
-          ),
-          $log->getLeaseID()),
-        $log->getMessage(),
+        $blueprint,
+        $resource,
+        $lease,
+        $type,
+        $data,
         phabricator_datetime($log->getEpoch(), $viewer),
       );
     }
 
     $table = new AphrontTableView($rows);
     $table->setDeviceReadyTable(true);
     $table->setHeaders(
       array(
+        pht('Blueprint'),
         pht('Resource'),
         pht('Lease'),
-        pht('Message'),
+        pht('Type'),
+        pht('Data'),
         pht('Date'),
       ));
-    $table->setShortHeaders(
-      array(
-        pht('R'),
-        pht('L'),
-        pht('Message'),
-        '',
-      ));
     $table->setColumnClasses(
       array(
+        '',
+        '',
         '',
         '',
         'wide',
         '',
       ));
 
     return $table;
   }
 
 }