Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[test visibility] Add option to automatically report logs within tests when using winston #4762

Merged
merged 9 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions integration-tests/automatic-log-submission.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
'use strict'

const { exec } = require('child_process')

const { assert } = require('chai')
const getPort = require('get-port')

const {
createSandbox,
getCiVisAgentlessConfig,
getCiVisEvpProxyConfig
} = require('./helpers')
const { FakeCiVisIntake } = require('./ci-visibility-intake')
const webAppServer = require('./ci-visibility/web-app-server')
const { NODE_MAJOR } = require('../version')

const cucumberVersion = NODE_MAJOR <= 16 ? '9' : 'latest'

describe('test visibility automatic log submission', () => {
let sandbox, cwd, receiver, childProcess, webAppPort
let testOutput = ''

before(async () => {
sandbox = await createSandbox([
'mocha',
`@cucumber/cucumber@${cucumberVersion}`,
'jest',
'winston',
'chai@4'
], true)
cwd = sandbox.folder
webAppPort = await getPort()
webAppServer.listen(webAppPort)
})

after(async () => {
await sandbox.remove()
await new Promise(resolve => webAppServer.close(resolve))
})

beforeEach(async function () {
const port = await getPort()
receiver = await new FakeCiVisIntake(port).start()
})

afterEach(async () => {
testOutput = ''
childProcess.kill()
await receiver.stop()
})

const testFrameworks = [
{
name: 'mocha',
command: 'mocha ./ci-visibility/automatic-log-submission/automatic-log-submission-test.js'
},
{
name: 'jest',
command: 'node ./node_modules/jest/bin/jest --config ./ci-visibility/automatic-log-submission/config-jest.js'
},
{
name: 'cucumber',
command: './node_modules/.bin/cucumber-js ci-visibility/automatic-log-submission-cucumber/*.feature'
}
]

testFrameworks.forEach(({ name, command }) => {
context(`with ${name}`, () => {
it('can automatically submit logs', (done) => {
const receiverPromise = receiver
.gatherPayloadsMaxTimeout(({ url }) => url.includes('/api/v2/logs'), payloads => {
const logMessages = payloads.flatMap(({ logMessage }) => logMessage)
const [url] = payloads.flatMap(({ url }) => url)

assert.equal(url, '/api/v2/logs?dd-api-key=1&ddsource=winston&service=my-service')
assert.equal(logMessages.length, 1)
const [{ dd, level, message }] = logMessages

assert.equal(level, 'info')
assert.equal(message, 'Hello simple log!')
assert.equal(dd.service, 'my-service')
assert.hasAllKeys(dd, ['trace_id', 'span_id', 'service'])
}).catch(done)

childProcess = exec(command,
{
cwd,
env: {
...getCiVisAgentlessConfig(receiver.port),
DD_AGENTLESS_LOG_SUBMISSION_ENABLED: '1',
DD_AGENTLESS_LOG_SUBMISSION_URL: `http://localhost:${receiver.port}`,
DD_API_KEY: '1',
DD_SERVICE: 'my-service'
},
stdio: 'pipe'
}
)
childProcess.on('exit', () => {
receiverPromise.then(() => {
assert.include(testOutput, 'Hello simple log!')
assert.include(testOutput, 'span_id')
done()
})
})

childProcess.stdout.on('data', (chunk) => {
testOutput += chunk.toString()
})
childProcess.stderr.on('data', (chunk) => {
testOutput += chunk.toString()
})
})

it('does not submit logs when DD_AGENTLESS_LOG_SUBMISSION_ENABLED is not set', (done) => {
childProcess = exec(command,
{
cwd,
env: {
...getCiVisAgentlessConfig(receiver.port),
DD_AGENTLESS_LOG_SUBMISSION_URL: `http://localhost:${receiver.port}`,
DD_SERVICE: 'my-service'
},
stdio: 'pipe'
}
)
childProcess.on('exit', () => {
assert.include(testOutput, 'Hello simple log!')
assert.notInclude(testOutput, 'span_id')
done()
})

childProcess.stdout.on('data', (chunk) => {
testOutput += chunk.toString()
})
childProcess.stderr.on('data', (chunk) => {
testOutput += chunk.toString()
})
})

it('does not submit logs when DD_AGENTLESS_LOG_SUBMISSION_ENABLED is set but DD_API_KEY is not', (done) => {
childProcess = exec(command,
{
cwd,
env: {
...getCiVisEvpProxyConfig(receiver.port),
DD_AGENTLESS_LOG_SUBMISSION_ENABLED: '1',
DD_AGENTLESS_LOG_SUBMISSION_URL: `http://localhost:${receiver.port}`,
DD_SERVICE: 'my-service',
DD_TRACE_DEBUG: '1',
DD_TRACE_LOG_LEVEL: 'warn',
DD_API_KEY: ''
},
stdio: 'pipe'
}
)
childProcess.on('exit', () => {
assert.include(testOutput, 'Hello simple log!')
assert.include(testOutput, 'no automatic log submission will be performed')
done()
})

childProcess.stdout.on('data', (chunk) => {
testOutput += chunk.toString()
})
childProcess.stderr.on('data', (chunk) => {
testOutput += chunk.toString()
})
})
})
})
})
9 changes: 9 additions & 0 deletions integration-tests/ci-visibility-intake.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,15 @@ class FakeCiVisIntake extends FakeAgent {
})
})

app.post('/api/v2/logs', express.json(), (req, res) => {
res.status(200).send('OK')
this.emit('message', {
headers: req.headers,
url: req.url,
logMessage: req.body
})
})

return new Promise((resolve, reject) => {
const timeoutObj = setTimeout(() => {
reject(new Error('Intake timed out starting up'))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Feature: Automatic Log Submission
Scenario: Run Automatic Log Submission
When we run a test
Then I should have made a log
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const { expect } = require('chai')
const { createLogger, format, transports } = require('winston')
const { When, Then } = require('@cucumber/cucumber')

const logger = createLogger({
level: 'info',
exitOnError: false,
format: format.json(),
transports: [
new transports.Console()
]
})

Then('I should have made a log', async function () {
expect(true).to.equal(true)
})

When('we run a test', async function () {
logger.log('info', 'Hello simple log!')
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const { createLogger, format, transports } = require('winston')
const { expect } = require('chai')

const logger = createLogger({
level: 'info',
exitOnError: false,
format: format.json(),
transports: [
new transports.Console()
]
})

describe('test', () => {
it('should return true', () => {
logger.log('info', 'Hello simple log!')

expect(true).to.be.true
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
projects: [],
testPathIgnorePatterns: ['/node_modules/'],
cache: false,
testMatch: [
'**/ci-visibility/automatic-log-submission/automatic-log-submission-*'
]
}
2 changes: 1 addition & 1 deletion integration-tests/selenium/selenium.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const {
} = require('../../packages/dd-trace/src/plugins/util/test')
const { NODE_MAJOR } = require('../../version')

const cucumberVersion = NODE_MAJOR <= 16 ? '9' : '10'
const cucumberVersion = NODE_MAJOR <= 16 ? '9' : 'latest'
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrelated fix


const webAppServer = require('../ci-visibility/web-app-server')

Expand Down
3 changes: 2 additions & 1 deletion packages/datadog-instrumentations/src/jest.js
Original file line number Diff line number Diff line change
Expand Up @@ -850,7 +850,8 @@ addHook({
}, jestConfigSyncWrapper)

const LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE = [
'selenium-webdriver'
'selenium-webdriver',
'winston'
]

function shouldBypassJestRequireEngine (moduleName) {
Expand Down
22 changes: 22 additions & 0 deletions packages/datadog-instrumentations/src/winston.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ const shimmer = require('../../datadog-shimmer')

const patched = new WeakSet()

// Test Visibility log submission channels
const configureCh = channel('ci:log-submission:winston:configure')
const addTransport = channel('ci:log-submission:winston:add-transport')

addHook({ name: 'winston', file: 'lib/winston/transports/index.js', versions: ['>=3'] }, transportsPackage => {
if (configureCh.hasSubscribers) {
configureCh.publish(transportsPackage.Http)
}

return transportsPackage
})

addHook({ name: 'winston', file: 'lib/winston/logger.js', versions: ['>=3'] }, Logger => {
const logCh = channel('apm:winston:log')
shimmer.wrap(Logger.prototype, 'write', write => {
Expand All @@ -20,6 +32,16 @@ addHook({ name: 'winston', file: 'lib/winston/logger.js', versions: ['>=3'] }, L
return write.apply(this, arguments)
}
})

shimmer.wrap(Logger.prototype, 'configure', configure => function () {
const configureResponse = configure.apply(this, arguments)
// After the original `configure`, because it resets transports
if (addTransport.hasSubscribers) {
addTransport.publish(this)
}
return configureResponse
})

return Logger
})

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const Plugin = require('../../plugins/plugin')
const log = require('../../log')

function getWinstonLogSubmissionParameters (config) {
const { site, service } = config

const defaultParameters = {
host: `http-intake.logs.${site}`,
path: `/api/v2/logs?dd-api-key=${process.env.DD_API_KEY}&ddsource=winston&service=${service}`,
ssl: true
}

if (!process.env.DD_AGENTLESS_LOG_SUBMISSION_URL) {
return defaultParameters
}

try {
const url = new URL(process.env.DD_AGENTLESS_LOG_SUBMISSION_URL)
return {
host: url.hostname,
port: url.port,
ssl: url.protocol === 'https:',
path: defaultParameters.path
}
} catch (e) {
log.error('Could not parse DD_AGENTLESS_LOG_SUBMISSION_URL')
return defaultParameters
}
}

class LogSubmissionPlugin extends Plugin {
static get id () {
return 'log-submission'
}

constructor (...args) {
super(...args)

this.addSub('ci:log-submission:winston:configure', (httpClass) => {
this.HttpClass = httpClass
})

this.addSub('ci:log-submission:winston:add-transport', (logger) => {
logger.add(new this.HttpClass(getWinstonLogSubmissionParameters(this.config)))
})
}
}

module.exports = LogSubmissionPlugin
5 changes: 4 additions & 1 deletion packages/dd-trace/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ class Config {
this._setValue(defaults, 'isIntelligentTestRunnerEnabled', false)
this._setValue(defaults, 'isManualApiEnabled', false)
this._setValue(defaults, 'ciVisibilityTestSessionName', '')
this._setValue(defaults, 'ciVisAgentlessLogSubmissionEnabled', false)
this._setValue(defaults, 'logInjection', false)
this._setValue(defaults, 'lookup', undefined)
this._setValue(defaults, 'memcachedCommandEnabled', false)
Expand Down Expand Up @@ -1035,7 +1036,8 @@ class Config {
DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED,
DD_CIVISIBILITY_FLAKY_RETRY_ENABLED,
DD_CIVISIBILITY_FLAKY_RETRY_COUNT,
DD_TEST_SESSION_NAME
DD_TEST_SESSION_NAME,
DD_AGENTLESS_LOG_SUBMISSION_ENABLED
} = process.env

if (DD_CIVISIBILITY_AGENTLESS_URL) {
Expand All @@ -1052,6 +1054,7 @@ class Config {
this._setBoolean(calc, 'isIntelligentTestRunnerEnabled', isTrue(this._isCiVisibilityItrEnabled()))
this._setBoolean(calc, 'isManualApiEnabled', !isFalse(this._isCiVisibilityManualApiEnabled()))
this._setString(calc, 'ciVisibilityTestSessionName', DD_TEST_SESSION_NAME)
this._setBoolean(calc, 'ciVisAgentlessLogSubmissionEnabled', isTrue(DD_AGENTLESS_LOG_SUBMISSION_ENABLED))
}
this._setString(calc, 'dogstatsd.hostname', this._getHostname())
this._setBoolean(calc, 'isGitUploadEnabled',
Expand Down
6 changes: 4 additions & 2 deletions packages/dd-trace/src/plugin_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ module.exports = class PluginManager {
dsmEnabled,
clientIpEnabled,
memcachedCommandEnabled,
ciVisibilityTestSessionName
ciVisibilityTestSessionName,
ciVisAgentlessLogSubmissionEnabled
} = this._tracerConfig

const sharedConfig = {
Expand All @@ -147,7 +148,8 @@ module.exports = class PluginManager {
site,
url,
headers: headerTags || [],
ciVisibilityTestSessionName
ciVisibilityTestSessionName,
ciVisAgentlessLogSubmissionEnabled
}

if (logInjection !== undefined) {
Expand Down
Loading
Loading