Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
2,756 changes: 2,615 additions & 141 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@
"@comapeo/ipc": "^2.1.0",
"@mapeo/default-config": "5.0.0",
"@mapeo/mock-data": "^5.0.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.202.0",
"@opentelemetry/sdk-node": "^0.202.0",
"@sentry/node": "^9.29.0",
"@sinonjs/fake-timers": "^10.0.2",
"@types/b4a": "^1.6.0",
"@types/bogon": "^1.0.2",
Expand Down Expand Up @@ -146,6 +149,7 @@
"nanobench": "^3.0.0",
"node-stream-zip": "^1.15.0",
"npm-run-all": "^4.1.5",
"opentelemetry-plugin-better-sqlite3": "^1.9.0",
"prettier": "^2.8.8",
"random-access-file": "^4.0.7",
"random-access-memory": "^6.2.1",
Expand All @@ -169,6 +173,8 @@
"@hyperswarm/secret-stream": "^6.6.3",
"@mapeo/crypto": "1.0.0-alpha.10",
"@mapeo/sqlite-indexer": "1.0.0-alpha.9",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/instrumentation": "^0.202.0",
"@sinclair/typebox": "^0.33.17",
"@sindresorhus/merge-streams": "^4.0.0",
"b4a": "^1.6.3",
Expand Down
117 changes: 117 additions & 0 deletions src/instrumentation/ActiveTracingHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { context as _context, trace } from '@opentelemetry/api'

/** @import { Context, Span, TracerProvider } from '@opentelemetry/api' */
/** @import { ExtendedSpanOptions, SpanCallback, TracingHelper } from './types.js' */

// https://www.w3.org/TR/trace-context/#examples-of-http-traceparent-headers
// If traceparent ends with -00 this trace will not be sampled
// the query engine needs the `10` for the span and trace id otherwise it does not parse this
const nonSampledTraceParent = `00-10-10-00`

/**
* @implements {TracingHelper}
*/
export class ActiveTracingHelper {
#tracerProvider

/**
* @param {object} opts
* @param {TracerProvider} opts.tracerProvider
*/
constructor({ tracerProvider }) {
this.#tracerProvider = tracerProvider
}

/**
* @returns {boolean}
*/
isEnabled() {
return true
}

/**
* @param {Context} [context]
* @returns {string}
*/
getTraceParent(context) {
const span = trace.getSpanContext(context ?? _context.active())
if (span) {
return `00-${span.traceId}-${span.spanId}-0${span.traceFlags}`
}
return nonSampledTraceParent
}

/**
* @returns {Context | undefined}
*/
getActiveContext() {
return _context.active()
}

/**
* @template R
* @param {string | ExtendedSpanOptions} options
* @param {SpanCallback<R>} callback
* @returns {R}
*/
runInChildSpan(options, callback) {
if (typeof options === 'string') {
options = { name: options }
}

const tracer = this.#tracerProvider.getTracer('comapeo')
const context = options.context ?? this.getActiveContext()
const name = `comapeo:core:${options.name}`

// these spans will not be nested by default even in recursive calls
// it's useful for showing middleware sequentially instead of nested
if (options.active === false) {
const span = tracer.startSpan(name, options, context)
return endSpan(span, callback(span, context))
}

// by default spans are "active", which means context is propagated in
// nested calls, which is useful for representing most of the calls
return tracer.startActiveSpan(name, options, (span) =>
endSpan(span, callback(span, context))
)
}
}

/**
* @template T
* @param {Span} span
* @param {T} result
* @returns {T}
*/
function endSpan(span, result) {
if (isPromiseLike(result)) {
return /** @type {T} */ (
result.then(
(value) => {
span.end()
return value
},
(reason) => {
span.end()
throw reason
}
)
)
}
span.end()
return result
}

/**
* @param {unknown} obj
* @returns {obj is PromiseLike<unknown>}
*/
function isPromiseLike(obj) {
return (
!!obj &&
(typeof obj === 'object' || typeof obj === 'function') &&
'then' in obj &&
typeof obj.then === 'function'
)
}
73 changes: 73 additions & 0 deletions src/instrumentation/ComapeoCoreInstrumentation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { trace } from '@opentelemetry/api'
import {
InstrumentationBase,
InstrumentationNodeModuleDefinition,
} from '@opentelemetry/instrumentation'

import { ActiveTracingHelper } from './ActiveTracingHelper.js'
import {
GLOBAL_INSTRUMENTATION_ACCESSOR_KEY,
MODULE_NAME,
NAME,
VERSION,
} from './constants.js'

/** @import {TracerProvider} from '@opentelemetry/api' */
/** @import {InstrumentationConfig} from '@opentelemetry/instrumentation' */
/** @import {ComapeoCoreInstrumentationGlobalValue} from './types.js' */

export class ComapeoCoreInstrumentation extends InstrumentationBase {
/**
* @type {TracerProvider | undefined}
* @private
*/
tracerProvider

/**
* @param {InstrumentationConfig} [config={}]
*/
constructor(config = {}) {
super(NAME, VERSION, config)
}

/**
* @param {TracerProvider} tracerProvider
* @returns {void}
*/
setTracerProvider(tracerProvider) {
this.tracerProvider = tracerProvider
}

/**
* @returns {InstrumentationNodeModuleDefinition[]}
*/
init() {
const module = new InstrumentationNodeModuleDefinition(MODULE_NAME, [
VERSION,
])

return [module]
}

enable() {
/** @type {ComapeoCoreInstrumentationGlobalValue} */
const globalValue = {
helper: new ActiveTracingHelper({
tracerProvider: this.tracerProvider ?? trace.getTracerProvider(),
}),
}

global[GLOBAL_INSTRUMENTATION_ACCESSOR_KEY] = globalValue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
global[GLOBAL_INSTRUMENTATION_ACCESSOR_KEY] = globalValue
globalThis[GLOBAL_INSTRUMENTATION_ACCESSOR_KEY] = globalValue

I prefer globalThis to global since it's the standard and is more future proof

}

disable() {
delete global[GLOBAL_INSTRUMENTATION_ACCESSOR_KEY]
}

/**
* @returns {boolean}
*/
isEnabled() {
return Boolean(global[GLOBAL_INSTRUMENTATION_ACCESSOR_KEY])
}
}
60 changes: 60 additions & 0 deletions src/instrumentation/TracingHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/** @import { TracingHelper } from './types.js' */

/** @type {TracingHelper} */
export const disabledTracingHelper = {
isEnabled() {
return false
},
getTraceParent() {
// https://www.w3.org/TR/trace-context/#examples-of-http-traceparent-headers
// If traceparent ends with -00 this trace will not be sampled
// the query engine needs the `10` for the span and trace id otherwise it does not parse this
return `00-10-10-00`
},

getActiveContext() {
return undefined
},

runInChildSpan(_, callback) {
return callback()
},
}

/**
* Tracing helper that can dynamically switch between enabled/disabled states
* Needed because tracing can be disabled and enabled with the calls to
* PrismaInstrumentation::disable/enable at any point
* @implements {TracingHelper}
*/
class DynamicTracingHelper {
isEnabled() {
return this.#getGlobalTracingHelper().isEnabled()
}
/** @type {TracingHelper['getTraceParent']} */
getTraceParent(context) {
return this.#getGlobalTracingHelper().getTraceParent(context)
}

getActiveContext() {
return this.#getGlobalTracingHelper().getActiveContext()
}

/** @type {TracingHelper['runInChildSpan']} */
runInChildSpan(options, callback) {
return this.#getGlobalTracingHelper().runInChildSpan(options, callback)
}

/** @returns {TracingHelper} */
#getGlobalTracingHelper() {
const fallbackPrismaInstrumentationGlobal =
globalThis.COMAPEO_CORE_INSTRUMENTATION

return fallbackPrismaInstrumentationGlobal?.helper ?? disabledTracingHelper
}
}

/** @returns {TracingHelper} */
export function getTracingHelper() {
return new DynamicTracingHelper()
}
8 changes: 8 additions & 0 deletions src/instrumentation/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const VERSION = '1.0.0'

export const GLOBAL_INSTRUMENTATION_ACCESSOR_KEY =
'COMAPEO_CORE_INSTRUMENTATION'

export const NAME = '@comapeo/core-instrumentation'

export const MODULE_NAME = '@comapeo/core'
52 changes: 52 additions & 0 deletions src/instrumentation/test-otel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
import { NodeSDK } from '@opentelemetry/sdk-node'
import { ComapeoCoreInstrumentation } from './ComapeoCoreInstrumentation.js'

// Initialize the NodeSDK
const sdk = new NodeSDK({
serviceName: 'comapeo-core-service-test',
traceExporter: new OTLPTraceExporter(),
instrumentations: [new ComapeoCoreInstrumentation()],
resourceDetectors: [],
})

// Start the SDK
sdk.start()

import { KeyManager } from '@mapeo/crypto'
import { MapeoManager } from '../mapeo-manager.js'
import RAM from 'random-access-memory'
import Fastify from 'fastify'

// Ensures batched spans are flushed before exit
process.on('beforeExit', async () => {
try {
await sdk.shutdown()
console.log('Tracing shut down successfully')
} catch (err) {
console.error('Error shutting down tracing', err)
} finally {
process.exit(0)
}
})

const projectMigrationsFolder = new URL(
'../../drizzle/project',
import.meta.url
).pathname
const clientMigrationsFolder = new URL('../../drizzle/client', import.meta.url)
.pathname

const manager = new MapeoManager({
rootKey: KeyManager.generateRootKey(),
projectMigrationsFolder,
clientMigrationsFolder,
dbFolder: ':memory:',
coreStorage: () => new RAM(),
fastify: Fastify(),
})

await manager.createProject({
name: 'project',
projectDescription: 'test project',
})
46 changes: 46 additions & 0 deletions src/instrumentation/test-sentry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as Sentry from '@sentry/node'
import { ComapeoCoreInstrumentation } from './ComapeoCoreInstrumentation.js'

Sentry.init({
dsn: 'https://fb7efc7b3426ff0de896fcf90c86cf47@o4507148235702272.ingest.us.sentry.io/4509510714785792',
debug: true,
tracesSampleRate: 1.0, // Adjust this value to control the sampling rate
openTelemetryInstrumentations: [new ComapeoCoreInstrumentation()],
})

import { KeyManager } from '@mapeo/crypto'
import { MapeoManager } from '../mapeo-manager.js'
import RAM from 'random-access-memory'
import Fastify from 'fastify'

process.on('beforeExit', async () => {
try {
await Sentry.flush(2000)
console.log('Tracing shut down successfully')
} catch (err) {
console.error('Error shutting down tracing', err)
} finally {
process.exit(0)
}
})

const projectMigrationsFolder = new URL(
'../../drizzle/project',
import.meta.url
).pathname
const clientMigrationsFolder = new URL('../../drizzle/client', import.meta.url)
.pathname

const manager = new MapeoManager({
rootKey: KeyManager.generateRootKey(),
projectMigrationsFolder,
clientMigrationsFolder,
dbFolder: ':memory:',
coreStorage: () => new RAM(),
fastify: Fastify(),
})

await manager.createProject({
name: 'project',
projectDescription: 'test project',
})
Loading