Skip to content

Commit

Permalink
Add metrics-patterns option (#1185)
Browse files Browse the repository at this point in the history
* Add metrics pattern filter

* Fix

* Fix

* Add doc

* Refactor

* Pin dependency
  • Loading branch information
int128 authored Aug 14, 2024

Unverified

This user has not yet uploaded their public signing key.
1 parent 4c3de6a commit 3014e78
Showing 11 changed files with 143 additions and 28 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -420,13 +420,43 @@ You can set the following inputs:
| `datadog-api-key` | - | Datadog API key. If not set, this action does not send metrics actually |
| `datadog-site` | - | Datadog Server name such as `datadoghq.eu`, `ddog-gov.com`, `us3.datadoghq.com` |
| `datadog-tags` | - | Additional tags in the form of `key:value` in a multiline string |
| `metrics-patterns` | - | Filter the metrics by patterns in a multiline string |
| `send-pull-request-labels` | `false` | Send pull request labels as Datadog tags |
| `collect-job-metrics` | `false` | Collect job metrics |
| `collect-step-metrics` | `false` | Collect step metrics |
| `prefer-distribution-workflow-run-metrics` | `false` | If true, send the distribution metrics instead of gauge metrics |
| `prefer-distribution-job-metrics` | `false` | If true, send the distribution metrics instead of gauge metrics |
| `prefer-distribution-step-metrics` | `false` | If true, send the distribution metrics instead of gauge metrics |

### Filter metrics

If `metrics-patterns` is set, this action sends the metrics filtered by the glob patterns.
The glob specification is same as [the filters of workflow](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow#using-filters).

To include the specific metrics,

```yaml
steps:
- uses: int128/datadog-actions-metrics@v1
with:
metrics-patterns: |
github.actions.workflow_run.*
github.actions.job.*
```

To exclude the specific metrics,

```yaml
steps:
- uses: int128/datadog-actions-metrics@v1
with:
metrics-patterns: |
*
!github.actions.*.conclusion.*
```

If both include and exclude patterns are given, the later pattern has higher precedence.

### Proxy

To connect to Datadog API via a HTTPS proxy, set `https_proxy` environment variable.
3 changes: 3 additions & 0 deletions action.yaml
Original file line number Diff line number Diff line change
@@ -20,6 +20,9 @@ inputs:
datadog-tags:
description: Additional tags in the form of `key:value` in a multiline string
required: false
metrics-patterns:
description: Filter the metrics by patterns in a multiline string
required: false

collect-job-metrics:
description: Collect job metrics
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -15,7 +15,8 @@
"@datadog/datadog-api-client": "1.27.0",
"@octokit/types": "12.6.0",
"@octokit/webhooks-types": "7.5.1",
"graphql": "16.9.0"
"graphql": "16.9.0",
"minimatch": "10.0.1"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.2",
11 changes: 11 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 10 additions & 15 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import * as core from '@actions/core'
import { client, v1 } from '@datadog/datadog-api-client'
import { HttpLibrary } from './http.js'
import { createMetricsFilter, MetricsFilter } from './filter.js'

type Inputs = {
datadogApiKey?: string
datadogSite?: string
datadogTags: string[]
metricsPatterns: string[]
}

export type MetricsClient = {
@@ -14,19 +16,19 @@ export type MetricsClient = {
}

class DryRunMetricsClient implements MetricsClient {
constructor(private readonly tags: string[]) {}
constructor(private readonly metricsFilter: MetricsFilter) {}

// eslint-disable-next-line @typescript-eslint/require-await
async submitMetrics(series: v1.Series[], description: string): Promise<void> {
series = injectTags(series, this.tags)
series = this.metricsFilter(series)
core.startGroup(`Metrics payload (dry-run) (${description})`)
core.info(JSON.stringify(series, undefined, 2))
core.endGroup()
}

// eslint-disable-next-line @typescript-eslint/require-await
async submitDistributionPoints(series: v1.DistributionPointsSeries[], description: string): Promise<void> {
series = injectTags(series, this.tags)
series = this.metricsFilter(series)
core.startGroup(`Distribution points payload (dry-run) (${description})`)
core.info(JSON.stringify(series, undefined, 2))
core.endGroup()
@@ -36,11 +38,11 @@ class DryRunMetricsClient implements MetricsClient {
class RealMetricsClient implements MetricsClient {
constructor(
private readonly metricsApi: v1.MetricsApi,
private readonly tags: string[],
private readonly metricsFilter: MetricsFilter,
) {}

async submitMetrics(series: v1.Series[], description: string): Promise<void> {
series = injectTags(series, this.tags)
series = this.metricsFilter(series)
core.startGroup(`Metrics payload (${description})`)
core.info(JSON.stringify(series, undefined, 2))
core.endGroup()
@@ -50,7 +52,7 @@ class RealMetricsClient implements MetricsClient {
}

async submitDistributionPoints(series: v1.DistributionPointsSeries[], description: string): Promise<void> {
series = injectTags(series, this.tags)
series = this.metricsFilter(series)
core.startGroup(`Distribution points payload (${description})`)
core.info(JSON.stringify(series, undefined, 2))
core.endGroup()
@@ -60,16 +62,9 @@ class RealMetricsClient implements MetricsClient {
}
}

export const injectTags = <S extends { tags?: string[] }>(series: S[], tags: string[]): S[] => {
if (tags.length === 0) {
return series
}
return series.map((s) => ({ ...s, tags: [...(s.tags ?? []), ...tags] }))
}

export const createMetricsClient = (inputs: Inputs): MetricsClient => {
if (inputs.datadogApiKey === undefined) {
return new DryRunMetricsClient(inputs.datadogTags)
return new DryRunMetricsClient(createMetricsFilter(inputs))
}

const configuration = client.createConfiguration({
@@ -83,7 +78,7 @@ export const createMetricsClient = (inputs: Inputs): MetricsClient => {
site: inputs.datadogSite,
})
}
return new RealMetricsClient(new v1.MetricsApi(configuration), inputs.datadogTags)
return new RealMetricsClient(new v1.MetricsApi(configuration), createMetricsFilter(inputs))
}

const createHttpLibraryIfHttpsProxy = () => {
41 changes: 41 additions & 0 deletions src/filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { minimatch } from 'minimatch'
import { v1 } from '@datadog/datadog-api-client'

type Inputs = {
metricsPatterns: string[]
datadogTags: string[]
}

export type MetricsFilter = <S extends v1.Series | v1.DistributionPointsSeries>(series: S[]) => S[]

export const createMetricsFilter = (inputs: Inputs): MetricsFilter => {
const matcher = createMatcher(inputs.metricsPatterns)
return (series) => {
series = series.filter((s) => matcher(s.metric))
return injectTags(series, inputs.datadogTags)
}
}

export const createMatcher =
(patterns: string[]) =>
(metric: string): boolean => {
if (patterns.length === 0) {
return true
}
let matched = false
for (const pattern of patterns) {
if (pattern.startsWith('!')) {
matched = matched && minimatch(metric, pattern)
} else {
matched = matched || minimatch(metric, pattern)
}
}
return matched
}

export const injectTags = <S extends { tags?: string[] }>(series: S[], tags: string[]): S[] => {
if (tags.length === 0) {
return series
}
return series.map((s) => ({ ...s, tags: [...(s.tags ?? []), ...tags] }))
}
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ const main = async (): Promise<void> => {
datadogApiKey: core.getInput('datadog-api-key') || undefined,
datadogSite: core.getInput('datadog-site') || undefined,
datadogTags: core.getMultilineInput('datadog-tags'),
metricsPatterns: core.getMultilineInput('metrics-patterns'),
collectJobMetrics: core.getBooleanInput('collect-job-metrics'),
collectStepMetrics: core.getBooleanInput('collect-step-metrics'),
preferDistributionWorkflowRunMetrics: core.getBooleanInput('prefer-distribution-workflow-run-metrics'),
1 change: 1 addition & 0 deletions src/run.ts
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ type Inputs = {
datadogApiKey?: string
datadogSite?: string
datadogTags: string[]
metricsPatterns: string[]
collectJobMetrics: boolean
collectStepMetrics: boolean
preferDistributionWorkflowRunMetrics: boolean
12 changes: 0 additions & 12 deletions tests/client.test.ts

This file was deleted.

40 changes: 40 additions & 0 deletions tests/filter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { createMatcher, injectTags } from '../src/filter.js'

describe('injectTags', () => {
it('should return series if tags is empty', () => {
const series = [{ tags: [] }]
expect(injectTags(series, [])).toEqual(series)
})
it('should return series with tags', () => {
const series = [{ tags: ['tag1:value1'] }]
expect(injectTags(series, ['tag2:value2'])).toEqual([{ tags: ['tag1:value1', 'tag2:value2'] }])
})
})

describe('createMatcher', () => {
it('should match anything when no pattern is given', () => {
const matcher = createMatcher([])
expect(matcher('github.foo.bar')).toBe(true)
expect(matcher('github.foo.baz')).toBe(true)
})
it('should match it when a pattern is given', () => {
const matcher = createMatcher(['*.bar'])
expect(matcher('github.foo.bar')).toBe(true)
expect(matcher('github.foo.baz')).toBe(false)
})
it('should exclude it when a negative pattern is given', () => {
const matcher = createMatcher(['*', '!*.github.*'])
expect(matcher('foo.github.bar')).toBe(false)
expect(matcher('foo.github.baz')).toBe(false)
expect(matcher('example.bar')).toBe(true)
expect(matcher('example.baz')).toBe(true)
})
it('should take higher precedence to the later pattern', () => {
// https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow#example-including-and-excluding-branches
const matcher = createMatcher(['*.bar', '!foo.*', '*.baz'])
expect(matcher('foo.github.bar')).toBe(false)
expect(matcher('foo.github.baz')).toBe(true)
expect(matcher('example.bar')).toBe(true)
expect(matcher('example.baz')).toBe(true)
})
})
4 changes: 4 additions & 0 deletions tests/run.test.ts
Original file line number Diff line number Diff line change
@@ -47,6 +47,7 @@ test('workflow_run with collectJobMetrics', async () => {
githubTokenForRateLimitMetrics: 'GITHUB_TOKEN',
datadogApiKey: 'DATADOG_API_KEY',
datadogTags: [],
metricsPatterns: [],
collectJobMetrics: true,
collectStepMetrics: true,
preferDistributionWorkflowRunMetrics: false,
@@ -75,6 +76,7 @@ test('workflow_run', async () => {
githubTokenForRateLimitMetrics: 'GITHUB_TOKEN',
datadogApiKey: 'DATADOG_API_KEY',
datadogTags: [],
metricsPatterns: [],
collectJobMetrics: false,
collectStepMetrics: false,
preferDistributionWorkflowRunMetrics: false,
@@ -103,6 +105,7 @@ test('pull_request_opened', async () => {
githubTokenForRateLimitMetrics: 'GITHUB_TOKEN',
datadogApiKey: 'DATADOG_API_KEY',
datadogTags: [],
metricsPatterns: [],
collectJobMetrics: false,
collectStepMetrics: false,
preferDistributionWorkflowRunMetrics: false,
@@ -132,6 +135,7 @@ test('pull_request_closed', async () => {
githubTokenForRateLimitMetrics: 'GITHUB_TOKEN',
datadogApiKey: 'DATADOG_API_KEY',
datadogTags: [],
metricsPatterns: [],
collectJobMetrics: false,
collectStepMetrics: false,
preferDistributionWorkflowRunMetrics: false,

0 comments on commit 3014e78

Please sign in to comment.