diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts
index 93d99b75fa0a..73cb76dd7af6 100644
--- a/client/src/api/schema/schema.ts
+++ b/client/src/api/schema/schema.ts
@@ -2594,6 +2594,23 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/api/invocations/{invocation_id}/metrics": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get Invocation Metrics */
+ get: operations["get_invocation_metrics_api_invocations__invocation_id__metrics_get"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/invocations/{invocation_id}/prepare_store_download": {
parameters: {
query?: never;
@@ -18317,6 +18334,45 @@ export interface components {
[key: string]: number;
};
};
+ /**
+ * WorkflowJobMetric
+ * @example {
+ * "name": "start_epoch",
+ * "plugin": "core",
+ * "raw_value": "1614261340.0000000",
+ * "title": "Job Start Time",
+ * "value": "2021-02-25 14:55:40"
+ * }
+ */
+ WorkflowJobMetric: {
+ /**
+ * Name
+ * @description The name of the metric variable.
+ */
+ name: string;
+ /**
+ * Plugin
+ * @description The instrumenter plugin that generated this metric.
+ */
+ plugin: string;
+ /**
+ * Raw Value
+ * @description The raw value of the metric as a string.
+ */
+ raw_value: string;
+ /**
+ * Title
+ * @description A descriptive title for this metric.
+ */
+ title: string;
+ /** Tool Id */
+ tool_id: string;
+ /**
+ * Value
+ * @description The textual representation of the metric value.
+ */
+ value: string;
+ };
/** WorkflowLandingRequest */
WorkflowLandingRequest: {
/** Request State */
@@ -27021,6 +27077,50 @@ export interface operations {
};
};
};
+ get_invocation_metrics_api_invocations__invocation_id__metrics_get: {
+ parameters: {
+ query?: never;
+ header?: {
+ /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
+ "run-as"?: string | null;
+ };
+ path: {
+ /** @description The encoded database identifier of the Invocation. */
+ invocation_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["WorkflowJobMetric"][];
+ };
+ };
+ /** @description Request Error */
+ "4XX": {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["MessageExceptionModel"];
+ };
+ };
+ /** @description Server Error */
+ "5XX": {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["MessageExceptionModel"];
+ };
+ };
+ };
+ };
prepare_store_download_api_invocations__invocation_id__prepare_store_download_post: {
parameters: {
query?: never;
diff --git a/client/src/components/WorkflowInvocationState/MetricsBoxPlots.vue b/client/src/components/WorkflowInvocationState/MetricsBoxPlots.vue
new file mode 100644
index 000000000000..1876190cc903
--- /dev/null
+++ b/client/src/components/WorkflowInvocationState/MetricsBoxPlots.vue
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/WorkflowInvocationState/WorkflowInvocationMetrics.vue b/client/src/components/WorkflowInvocationState/WorkflowInvocationMetrics.vue
new file mode 100644
index 000000000000..cb9aed99233f
--- /dev/null
+++ b/client/src/components/WorkflowInvocationState/WorkflowInvocationMetrics.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
+
diff --git a/client/src/components/WorkflowInvocationState/WorkflowInvocationState.vue b/client/src/components/WorkflowInvocationState/WorkflowInvocationState.vue
index e035bc9b7dbe..bcf03f61b816 100644
--- a/client/src/components/WorkflowInvocationState/WorkflowInvocationState.vue
+++ b/client/src/components/WorkflowInvocationState/WorkflowInvocationState.vue
@@ -13,6 +13,7 @@ import { isTerminal, jobCount, runningCount } from "./util";
import InvocationReport from "../Workflow/InvocationReport.vue";
import WorkflowInvocationExportOptions from "./WorkflowInvocationExportOptions.vue";
+import WorkflowInvocationMetrics from "./WorkflowInvocationMetrics.vue";
import WorkflowInvocationHeader from "./WorkflowInvocationHeader.vue";
import WorkflowInvocationInputOutputTabs from "./WorkflowInvocationInputOutputTabs.vue";
import WorkflowInvocationOverview from "./WorkflowInvocationOverview.vue";
@@ -185,6 +186,9 @@ function cancelWorkflowSchedulingLocal() {
+
+
+
diff --git a/lib/galaxy/managers/jobs.py b/lib/galaxy/managers/jobs.py
index 1d4a61dac6f0..be9d0083567b 100644
--- a/lib/galaxy/managers/jobs.py
+++ b/lib/galaxy/managers/jobs.py
@@ -54,6 +54,7 @@
ImplicitCollectionJobs,
ImplicitCollectionJobsJobAssociation,
Job,
+ JobMetricNumeric,
JobParameter,
User,
Workflow,
@@ -729,6 +730,38 @@ def invocation_job_source_iter(sa_session, invocation_id):
yield ("ImplicitCollectionJobs", row[1], row[2])
+def get_job_metrics_for_invocation(sa_session: galaxy_scoped_session, invocation_id: int):
+ single_job_stmnt = (
+ select(JobMetricNumeric, Job.tool_id)
+ .join(Job, JobMetricNumeric.job_id == Job.id)
+ .join(
+ WorkflowInvocationStep,
+ and_(
+ WorkflowInvocationStep.workflow_invocation_id == invocation_id, WorkflowInvocationStep.job_id == Job.id
+ ),
+ )
+ )
+ collection_job_stmnt = (
+ select(JobMetricNumeric, Job.tool_id)
+ .join(Job, JobMetricNumeric.job_id == Job.id)
+ .join(ImplicitCollectionJobsJobAssociation, Job.id == ImplicitCollectionJobsJobAssociation.job_id)
+ .join(
+ ImplicitCollectionJobs,
+ ImplicitCollectionJobs.id == ImplicitCollectionJobsJobAssociation.implicit_collection_jobs_id,
+ )
+ .join(
+ WorkflowInvocationStep,
+ and_(
+ WorkflowInvocationStep.workflow_invocation_id == invocation_id,
+ WorkflowInvocationStep.implicit_collection_jobs_id == ImplicitCollectionJobs.id,
+ ),
+ )
+ )
+ # should be sa_session.execute(single_job_stmnt.union(collection_job_stmnt)).all() but that returns
+ # columns instead of the job metrics ORM instance.
+ return list(sa_session.execute(single_job_stmnt).all()) + list(sa_session.execute(collection_job_stmnt).all())
+
+
def fetch_job_states(sa_session, job_source_ids, job_source_types):
assert len(job_source_ids) == len(job_source_types)
job_ids = set()
@@ -911,6 +944,10 @@ def summarize_job_metrics(trans, job):
Precondition: the caller has verified the job is accessible to the user
represented by the trans parameter.
"""
+ return summarize_metrics(trans, job.metrics)
+
+
+def summarize_metrics(trans: ProvidesUserContext, job_metrics):
safety_level = Safety.SAFE
if trans.user_is_admin:
safety_level = Safety.UNSAFE
@@ -922,7 +959,7 @@ def summarize_job_metrics(trans, job):
m.metric_value,
m.plugin,
)
- for m in job.metrics
+ for m in job_metrics
]
dictifiable_metrics = trans.app.job_metrics.dictifiable_metrics(raw_metrics, safety_level)
return [d.dict() for d in dictifiable_metrics]
diff --git a/lib/galaxy/schema/schema.py b/lib/galaxy/schema/schema.py
index 0d9646316e59..88460a3f425f 100644
--- a/lib/galaxy/schema/schema.py
+++ b/lib/galaxy/schema/schema.py
@@ -2171,6 +2171,10 @@ class JobMetric(Model):
)
+class WorkflowJobMetric(JobMetric):
+ tool_id: str
+
+
class JobMetricCollection(RootModel):
"""Represents a collection of metrics associated with a Job."""
diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py
index c4abb88968c0..e3604058bc4f 100644
--- a/lib/galaxy/webapps/galaxy/api/workflows.py
+++ b/lib/galaxy/webapps/galaxy/api/workflows.py
@@ -78,6 +78,7 @@
ShareWithPayload,
ShareWithStatus,
SharingStatus,
+ WorkflowJobMetric,
WorkflowLandingRequest,
WorkflowSortByEnum,
)
@@ -1759,3 +1760,11 @@ def workflow_invocation_jobs_summary(
) -> InvocationJobsResponse:
"""An alias for `GET /api/invocations/{invocation_id}/jobs_summary`. `workflow_id` is ignored."""
return self.invocation_jobs_summary(trans=trans, invocation_id=invocation_id)
+
+ @router.get("/api/invocations/{invocation_id}/metrics")
+ def get_invocation_metrics(
+ self,
+ invocation_id: InvocationIDPathParam,
+ trans: ProvidesUserContext = DependsOnTrans,
+ ) -> List[WorkflowJobMetric]:
+ return self.invocations_service.show_invocation_metrics(trans=trans, invocation_id=invocation_id)
diff --git a/lib/galaxy/webapps/galaxy/services/invocations.py b/lib/galaxy/webapps/galaxy/services/invocations.py
index ce9610ac0422..4066a3dbfeaa 100644
--- a/lib/galaxy/webapps/galaxy/services/invocations.py
+++ b/lib/galaxy/webapps/galaxy/services/invocations.py
@@ -25,7 +25,9 @@
from galaxy.managers.histories import HistoryManager
from galaxy.managers.jobs import (
fetch_job_states,
+ get_job_metrics_for_invocation,
invocation_job_source_iter,
+ summarize_metrics,
)
from galaxy.managers.workflows import WorkflowsManager
from galaxy.model import (
@@ -162,6 +164,18 @@ def show_invocation_step(self, trans, step_id) -> InvocationStep:
)
return self.serialize_workflow_invocation_step(wfi_step)
+ def show_invocation_metrics(self, trans: ProvidesHistoryContext, invocation_id: int):
+ extended_job_metrics = get_job_metrics_for_invocation(trans.sa_session, invocation_id)
+ job_metrics = []
+ tool_ids = []
+ for row in extended_job_metrics:
+ job_metrics.append(row[0])
+ tool_ids.append(row[1])
+ metrics_dict_list = summarize_metrics(trans, job_metrics)
+ for tool_id, metrics_dict in zip(tool_ids, metrics_dict_list):
+ metrics_dict["tool_id"] = tool_id
+ return metrics_dict_list
+
def update_invocation_step(self, trans, step_id, action):
wfi_step = self._workflows_manager.update_invocation_step(trans, step_id, action)
return self.serialize_workflow_invocation_step(wfi_step)
diff --git a/lib/galaxy_test/api/test_workflows.py b/lib/galaxy_test/api/test_workflows.py
index 174985ad51d9..f18cdd8a760d 100644
--- a/lib/galaxy_test/api/test_workflows.py
+++ b/lib/galaxy_test/api/test_workflows.py
@@ -3499,6 +3499,39 @@ def test_workflow_new_autocreated_history(self):
invocation_id = run_workflow_dict["id"]
self.workflow_populator.wait_for_invocation_and_jobs(new_history_id, workflow_id, invocation_id)
+ def test_invocation_job_metrics_simple(self):
+ with self.dataset_populator.test_history() as history_id:
+ summary = self._run_workflow(WORKFLOW_SIMPLE, test_data={"input1": "hello world"}, history_id=history_id)
+ self.workflow_populator.wait_for_invocation_and_jobs(
+ history_id=history_id, workflow_id=summary.workflow_id, invocation_id=summary.invocation_id
+ )
+ job_metrics = self._get(f"invocations/{summary.invocation_id}/metrics").json()
+ galaxy_slots = [m for m in job_metrics if m["name"] == "galaxy_slots"]
+ assert len(galaxy_slots) == 1
+
+ def test_invocation_job_metrics_map_over(self):
+ with self.dataset_populator.test_history() as history_id:
+ summary = self._run_workflow(
+ WORKFLOW_SIMPLE,
+ test_data={
+ "input1": {
+ "collection_type": "list",
+ "name": "the_dataset_list",
+ "elements": [
+ {"identifier": "el1", "value": "1.fastq", "type": "File"},
+ {"identifier": "el2", "value": "1.fastq", "type": "File"},
+ ],
+ }
+ },
+ history_id=history_id,
+ )
+ self.workflow_populator.wait_for_invocation_and_jobs(
+ history_id=history_id, workflow_id=summary.workflow_id, invocation_id=summary.invocation_id
+ )
+ job_metrics = self._get(f"invocations/{summary.invocation_id}/metrics").json()
+ galaxy_slots = [m for m in job_metrics if m["name"] == "galaxy_slots"]
+ assert len(galaxy_slots) == 2
+
def test_workflow_output_dataset(self):
with self.dataset_populator.test_history() as history_id:
summary = self._run_workflow(WORKFLOW_SIMPLE, test_data={"input1": "hello world"}, history_id=history_id)