mirror of
https://github.com/imjasonh/gcp-metrics-action
synced 2026-07-22 07:30:11 +00:00
fix job conclusion, remove step.total
Signed-off-by: Jason Hall <jason@chainguard.dev>
This commit is contained in:
parent
6cbadacc27
commit
69a4789f22
7 changed files with 217 additions and 55 deletions
14
README.md
14
README.md
|
|
@ -247,11 +247,6 @@ steps:
|
|||
- `step.status` - Status (completed, in_progress, etc.)
|
||||
- `step.conclusion` - Conclusion (success, failure, skipped, etc.)
|
||||
|
||||
#### Step Count
|
||||
- **Metric:** `github.actions.step.total`
|
||||
- **Type:** Counter
|
||||
- **Labels:** Same as step duration
|
||||
|
||||
### Traces
|
||||
|
||||
The action creates distributed traces showing the execution timeline of your workflow:
|
||||
|
|
@ -286,7 +281,6 @@ Metrics will appear in Google Cloud Monitoring under custom metrics:
|
|||
3. Available metrics:
|
||||
- `custom.googleapis.com/github.actions/job.duration`
|
||||
- `custom.googleapis.com/github.actions/step.duration`
|
||||
- `custom.googleapis.com/github.actions/step.total`
|
||||
|
||||

|
||||
|
||||
|
|
@ -300,11 +294,11 @@ custom.googleapis.com/github.actions/step.duration
|
|||
| mean
|
||||
```
|
||||
|
||||
**Job failure rate:**
|
||||
**Job failure rate over time:**
|
||||
```
|
||||
custom.googleapis.com/github.actions/step.total
|
||||
| filter metric.step.conclusion = "failure"
|
||||
| rate(1m)
|
||||
custom.googleapis.com/github.actions/job.duration
|
||||
| filter metric.job.conclusion = "failure"
|
||||
| group_by [], .rate(1h)
|
||||
```
|
||||
|
||||
**Metrics for a specific PR:**
|
||||
|
|
|
|||
9
dist/index.js
vendored
9
dist/index.js
vendored
|
|
@ -366,12 +366,6 @@ function recordMetrics(meter, metrics, metricPrefix) {
|
|||
});
|
||||
core.debug(`Created histogram metric: ${stepDurationName}`);
|
||||
|
||||
const stepCounterName = `${metricPrefix}.step.total`;
|
||||
const stepCounter = meter.createCounter(stepCounterName, {
|
||||
description: 'Total count of workflow steps by conclusion',
|
||||
});
|
||||
core.debug(`Created counter metric: ${stepCounterName}`);
|
||||
|
||||
// Record job-level metrics (always record, even if job not completed yet)
|
||||
const jobDurationHistogram = meter.createHistogram(`${metricPrefix}.job.duration`, {
|
||||
description: 'Duration of workflow jobs in milliseconds',
|
||||
|
|
@ -401,9 +395,6 @@ function recordMetrics(meter, metrics, metricPrefix) {
|
|||
stepDurationHistogram.record(step.durationMs, stepAttributes);
|
||||
core.info(`Recorded step "${step.name}" duration: ${step.durationMs}ms`);
|
||||
}
|
||||
|
||||
// Record step count (for success/failure tracking)
|
||||
stepCounter.add(1, stepAttributes);
|
||||
}
|
||||
|
||||
core.info(`Recorded metrics for ${metrics.steps.length} steps`);
|
||||
|
|
|
|||
34
dist/post/index.js
vendored
34
dist/post/index.js
vendored
|
|
@ -74,6 +74,27 @@ async function collectMetrics(octokit, context) {
|
|||
core.debug(`Job not marked complete yet, estimating duration using current time: ${jobDurationMs}ms`);
|
||||
}
|
||||
|
||||
// Infer job conclusion from steps if not set (post-action runs before job completes)
|
||||
let jobConclusion = job.conclusion;
|
||||
if (!jobConclusion || jobConclusion === 'unknown') {
|
||||
const hasFailure = steps.some(s => s.conclusion === 'failure');
|
||||
const hasCancelled = steps.some(s => s.conclusion === 'cancelled');
|
||||
|
||||
if (hasFailure) {
|
||||
jobConclusion = 'failure';
|
||||
core.debug('Inferred job conclusion as "failure" based on failed steps');
|
||||
} else if (hasCancelled) {
|
||||
jobConclusion = 'cancelled';
|
||||
core.debug('Inferred job conclusion as "cancelled" based on cancelled steps');
|
||||
} else if (steps.length > 0 && steps.every(s => s.conclusion === 'success' || s.conclusion === 'skipped')) {
|
||||
jobConclusion = 'success';
|
||||
core.debug('Inferred job conclusion as "success" based on step results');
|
||||
} else {
|
||||
jobConclusion = 'unknown';
|
||||
core.debug('Could not infer job conclusion from steps');
|
||||
}
|
||||
}
|
||||
|
||||
// Extract PR number if this is a pull request event
|
||||
const prNumber = context.payload?.pull_request?.number ||
|
||||
(process.env.GITHUB_REF?.match(/refs\/pull\/(\d+)\/merge/)?.[1]) ||
|
||||
|
|
@ -84,8 +105,8 @@ async function collectMetrics(octokit, context) {
|
|||
job: {
|
||||
name: job.name,
|
||||
id: job.id,
|
||||
status: job.status,
|
||||
conclusion: job.conclusion,
|
||||
status: job.status || 'in_progress',
|
||||
conclusion: jobConclusion,
|
||||
startedAt: jobStartedAt,
|
||||
completedAt: jobCompletedAt,
|
||||
durationMs: jobDurationMs,
|
||||
|
|
@ -493,12 +514,6 @@ function recordMetrics(meter, metrics, metricPrefix) {
|
|||
});
|
||||
core.debug(`Created histogram metric: ${stepDurationName}`);
|
||||
|
||||
const stepCounterName = `${metricPrefix}.step.total`;
|
||||
const stepCounter = meter.createCounter(stepCounterName, {
|
||||
description: 'Total count of workflow steps by conclusion',
|
||||
});
|
||||
core.debug(`Created counter metric: ${stepCounterName}`);
|
||||
|
||||
// Record job-level metrics (always record, even if job not completed yet)
|
||||
const jobDurationHistogram = meter.createHistogram(`${metricPrefix}.job.duration`, {
|
||||
description: 'Duration of workflow jobs in milliseconds',
|
||||
|
|
@ -528,9 +543,6 @@ function recordMetrics(meter, metrics, metricPrefix) {
|
|||
stepDurationHistogram.record(step.durationMs, stepAttributes);
|
||||
core.info(`Recorded step "${step.name}" duration: ${step.durationMs}ms`);
|
||||
}
|
||||
|
||||
// Record step count (for success/failure tracking)
|
||||
stepCounter.add(1, stepAttributes);
|
||||
}
|
||||
|
||||
core.info(`Recorded metrics for ${metrics.steps.length} steps`);
|
||||
|
|
|
|||
|
|
@ -68,6 +68,27 @@ async function collectMetrics(octokit, context) {
|
|||
core.debug(`Job not marked complete yet, estimating duration using current time: ${jobDurationMs}ms`);
|
||||
}
|
||||
|
||||
// Infer job conclusion from steps if not set (post-action runs before job completes)
|
||||
let jobConclusion = job.conclusion;
|
||||
if (!jobConclusion || jobConclusion === 'unknown') {
|
||||
const hasFailure = steps.some(s => s.conclusion === 'failure');
|
||||
const hasCancelled = steps.some(s => s.conclusion === 'cancelled');
|
||||
|
||||
if (hasFailure) {
|
||||
jobConclusion = 'failure';
|
||||
core.debug('Inferred job conclusion as "failure" based on failed steps');
|
||||
} else if (hasCancelled) {
|
||||
jobConclusion = 'cancelled';
|
||||
core.debug('Inferred job conclusion as "cancelled" based on cancelled steps');
|
||||
} else if (steps.length > 0 && steps.every(s => s.conclusion === 'success' || s.conclusion === 'skipped')) {
|
||||
jobConclusion = 'success';
|
||||
core.debug('Inferred job conclusion as "success" based on step results');
|
||||
} else {
|
||||
jobConclusion = 'unknown';
|
||||
core.debug('Could not infer job conclusion from steps');
|
||||
}
|
||||
}
|
||||
|
||||
// Extract PR number if this is a pull request event
|
||||
const prNumber = context.payload?.pull_request?.number ||
|
||||
(process.env.GITHUB_REF?.match(/refs\/pull\/(\d+)\/merge/)?.[1]) ||
|
||||
|
|
@ -78,8 +99,8 @@ async function collectMetrics(octokit, context) {
|
|||
job: {
|
||||
name: job.name,
|
||||
id: job.id,
|
||||
status: job.status,
|
||||
conclusion: job.conclusion,
|
||||
status: job.status || 'in_progress',
|
||||
conclusion: jobConclusion,
|
||||
startedAt: jobStartedAt,
|
||||
completedAt: jobCompletedAt,
|
||||
durationMs: jobDurationMs,
|
||||
|
|
|
|||
|
|
@ -106,12 +106,6 @@ function recordMetrics(meter, metrics, metricPrefix) {
|
|||
});
|
||||
core.debug(`Created histogram metric: ${stepDurationName}`);
|
||||
|
||||
const stepCounterName = `${metricPrefix}.step.total`;
|
||||
const stepCounter = meter.createCounter(stepCounterName, {
|
||||
description: 'Total count of workflow steps by conclusion',
|
||||
});
|
||||
core.debug(`Created counter metric: ${stepCounterName}`);
|
||||
|
||||
// Record job-level metrics (always record, even if job not completed yet)
|
||||
const jobDurationHistogram = meter.createHistogram(`${metricPrefix}.job.duration`, {
|
||||
description: 'Duration of workflow jobs in milliseconds',
|
||||
|
|
@ -141,9 +135,6 @@ function recordMetrics(meter, metrics, metricPrefix) {
|
|||
stepDurationHistogram.record(step.durationMs, stepAttributes);
|
||||
core.info(`Recorded step "${step.name}" duration: ${step.durationMs}ms`);
|
||||
}
|
||||
|
||||
// Record step count (for success/failure tracking)
|
||||
stepCounter.add(1, stepAttributes);
|
||||
}
|
||||
|
||||
core.info(`Recorded metrics for ${metrics.steps.length} steps`);
|
||||
|
|
|
|||
|
|
@ -171,4 +171,167 @@ test('collectMetrics', async (t) => {
|
|||
/API Error/
|
||||
);
|
||||
});
|
||||
|
||||
await t.test('should infer job conclusion as failure from failed steps', async () => {
|
||||
const mockJobData = {
|
||||
jobs: [
|
||||
{
|
||||
id: 12345,
|
||||
name: 'test-job',
|
||||
status: 'in_progress',
|
||||
conclusion: null,
|
||||
started_at: '2025-01-01T10:00:00Z',
|
||||
completed_at: null,
|
||||
steps: [
|
||||
{
|
||||
name: 'Step 1',
|
||||
number: 1,
|
||||
status: 'completed',
|
||||
conclusion: 'success',
|
||||
started_at: '2025-01-01T10:00:00Z',
|
||||
completed_at: '2025-01-01T10:01:00Z',
|
||||
},
|
||||
{
|
||||
name: 'Step 2',
|
||||
number: 2,
|
||||
status: 'completed',
|
||||
conclusion: 'failure',
|
||||
started_at: '2025-01-01T10:01:00Z',
|
||||
completed_at: '2025-01-01T10:02:00Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockOctokit = {
|
||||
rest: {
|
||||
actions: {
|
||||
listJobsForWorkflowRun: mock.fn(async () => ({ data: mockJobData })),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockContext = {
|
||||
repo: { owner: 'test-owner', repo: 'test-repo' },
|
||||
runId: 67890,
|
||||
runNumber: 42,
|
||||
workflow: 'CI',
|
||||
};
|
||||
|
||||
process.env.GITHUB_JOB = 'test-job';
|
||||
|
||||
const metrics = await collectMetrics(mockOctokit, mockContext);
|
||||
|
||||
assert.strictEqual(metrics.job.conclusion, 'failure');
|
||||
assert.strictEqual(metrics.job.status, 'in_progress');
|
||||
});
|
||||
|
||||
await t.test('should infer job conclusion as success from successful steps', async () => {
|
||||
const mockJobData = {
|
||||
jobs: [
|
||||
{
|
||||
id: 12345,
|
||||
name: 'test-job',
|
||||
status: 'in_progress',
|
||||
conclusion: null,
|
||||
started_at: '2025-01-01T10:00:00Z',
|
||||
completed_at: null,
|
||||
steps: [
|
||||
{
|
||||
name: 'Step 1',
|
||||
number: 1,
|
||||
status: 'completed',
|
||||
conclusion: 'success',
|
||||
started_at: '2025-01-01T10:00:00Z',
|
||||
completed_at: '2025-01-01T10:01:00Z',
|
||||
},
|
||||
{
|
||||
name: 'Step 2',
|
||||
number: 2,
|
||||
status: 'completed',
|
||||
conclusion: 'success',
|
||||
started_at: '2025-01-01T10:01:00Z',
|
||||
completed_at: '2025-01-01T10:02:00Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockOctokit = {
|
||||
rest: {
|
||||
actions: {
|
||||
listJobsForWorkflowRun: mock.fn(async () => ({ data: mockJobData })),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockContext = {
|
||||
repo: { owner: 'test-owner', repo: 'test-repo' },
|
||||
runId: 67890,
|
||||
runNumber: 42,
|
||||
workflow: 'CI',
|
||||
};
|
||||
|
||||
process.env.GITHUB_JOB = 'test-job';
|
||||
|
||||
const metrics = await collectMetrics(mockOctokit, mockContext);
|
||||
|
||||
assert.strictEqual(metrics.job.conclusion, 'success');
|
||||
});
|
||||
|
||||
await t.test('should infer job conclusion as cancelled from cancelled steps', async () => {
|
||||
const mockJobData = {
|
||||
jobs: [
|
||||
{
|
||||
id: 12345,
|
||||
name: 'test-job',
|
||||
status: 'in_progress',
|
||||
conclusion: null,
|
||||
started_at: '2025-01-01T10:00:00Z',
|
||||
completed_at: null,
|
||||
steps: [
|
||||
{
|
||||
name: 'Step 1',
|
||||
number: 1,
|
||||
status: 'completed',
|
||||
conclusion: 'success',
|
||||
started_at: '2025-01-01T10:00:00Z',
|
||||
completed_at: '2025-01-01T10:01:00Z',
|
||||
},
|
||||
{
|
||||
name: 'Step 2',
|
||||
number: 2,
|
||||
status: 'completed',
|
||||
conclusion: 'cancelled',
|
||||
started_at: '2025-01-01T10:01:00Z',
|
||||
completed_at: '2025-01-01T10:02:00Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockOctokit = {
|
||||
rest: {
|
||||
actions: {
|
||||
listJobsForWorkflowRun: mock.fn(async () => ({ data: mockJobData })),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockContext = {
|
||||
repo: { owner: 'test-owner', repo: 'test-repo' },
|
||||
runId: 67890,
|
||||
runNumber: 42,
|
||||
workflow: 'CI',
|
||||
};
|
||||
|
||||
process.env.GITHUB_JOB = 'test-job';
|
||||
|
||||
const metrics = await collectMetrics(mockOctokit, mockContext);
|
||||
|
||||
assert.strictEqual(metrics.job.conclusion, 'cancelled');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -137,23 +137,15 @@ test('recordMetrics', async (t) => {
|
|||
// Verify histogram creation for job and step duration
|
||||
assert.strictEqual(mockMeter.createHistogram.mock.calls.length >= 2, true);
|
||||
|
||||
// Verify counter creation for step totals
|
||||
assert.strictEqual(mockMeter.createCounter.mock.calls.length >= 1, true);
|
||||
|
||||
// Verify histogram records were called (1 job + 2 steps = 3)
|
||||
assert.strictEqual(mockHistogramRecord.mock.calls.length, 3);
|
||||
|
||||
// Verify counter was called for each step
|
||||
assert.strictEqual(mockCounterAdd.mock.calls.length, 2);
|
||||
});
|
||||
|
||||
await t.test('should handle steps with zero duration', () => {
|
||||
const mockHistogramRecord = mock.fn();
|
||||
const mockCounterAdd = mock.fn();
|
||||
|
||||
const mockMeter = {
|
||||
createHistogram: mock.fn(() => ({ record: mockHistogramRecord })),
|
||||
createCounter: mock.fn(() => ({ add: mockCounterAdd })),
|
||||
};
|
||||
|
||||
const metrics = {
|
||||
|
|
@ -200,19 +192,17 @@ test('recordMetrics', async (t) => {
|
|||
|
||||
recordMetrics(mockMeter, metrics, 'test.prefix');
|
||||
|
||||
// Job duration is 0, so it should not be recorded
|
||||
// Step has 0 duration, so histogram should not record it
|
||||
// But counter should still be called for the step
|
||||
assert.strictEqual(mockCounterAdd.mock.calls.length, 1);
|
||||
// Job duration is always recorded, even if 0
|
||||
// Step has 0 duration, so it should not be recorded
|
||||
// Total histogram calls: 1 (just the job)
|
||||
assert.strictEqual(mockHistogramRecord.mock.calls.length, 1);
|
||||
});
|
||||
|
||||
await t.test('should include correct attributes in metrics', () => {
|
||||
const mockHistogramRecord = mock.fn();
|
||||
const mockCounterAdd = mock.fn();
|
||||
|
||||
const mockMeter = {
|
||||
createHistogram: mock.fn(() => ({ record: mockHistogramRecord })),
|
||||
createCounter: mock.fn(() => ({ add: mockCounterAdd })),
|
||||
};
|
||||
|
||||
const metrics = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue