about summary refs log tree commit diff
path: root/users/Profpatsch/alacritty-change-color-scheme
diff options
context:
space:
mode:
authorProfpatsch <mail@profpatsch.de>2024-12-09T20·10+0100
committerProfpatsch <mail@profpatsch.de>2024-12-10T15·34+0000
commit4eeac3cb1dcf933482d00169140a09a7c01f3a3f (patch)
treea097d754889833ad230be83d4a6e8a5bc76638af /users/Profpatsch/alacritty-change-color-scheme
parent821ff7ffe4d1ef5546084b57d65240c23babc453 (diff)
feat(users/Profpatsch): Implement initial otel-dbus proxy r/8998
For simplicity’s sake this puts everything into the
alacritty-change-color-scheme script for now.

This implements a simple dbus-opentelemetry proxy adapter, which
allows services to record otel traces without having to depend on the
quite complex otel libraries. Instead, they just send their traces to
the dbus tracing interface, and the service that binds against that
interface forwards the spans to the OTLP collector.

First you create a new Tracer for your service via the `TracerFactory`
interface:

```
> busctl --user call \
    de.profpatsch.otel.Tracer \
    /de/profpatsch/otel/TracerFactory \
    de.profpatsch.otel.TracerFactory CreateTracer \
    s hello
s "/de/profpatsch/otel/tracers/hello"
```

(this corresponds to setting up a tracer with properties in OTEL)

Then, you can use the returned object path to call the `Tracer`
interface proper:

```
< busctl --user call \
    de.profpatsch.otel.Tracer \
    /de/profpatsch/otel/tracers/hello \
    de.profpatsch.otel.Tracer \
    StartSpan \
    s '{"spanId": "111", "name": "111"}'
```

This will create the spans. You can also set their timestamps on the
sending side via `startTime`/`endTime`, but make sure it’s a hrtime
tuple.

Prefer batching multiple spans vie the `BatchSpans` call.

Change-Id: Ie6cfdcb0dc3e2398316a2c1763bc72c1118168b0
Reviewed-on: https://cl.tvl.fyi/c/depot/+/12885
Tested-by: BuildkiteCI
Reviewed-by: Profpatsch <mail@profpatsch.de>
Reviewed-by: benjaminedwardwebb <benjaminedwardwebb@gmail.com>
Diffstat (limited to 'users/Profpatsch/alacritty-change-color-scheme')
-rw-r--r--users/Profpatsch/alacritty-change-color-scheme/alacritty-change-color-scheme.js521
-rw-r--r--users/Profpatsch/alacritty-change-color-scheme/package-lock.json296
-rw-r--r--users/Profpatsch/alacritty-change-color-scheme/package.json5
3 files changed, 755 insertions, 67 deletions
diff --git a/users/Profpatsch/alacritty-change-color-scheme/alacritty-change-color-scheme.js b/users/Profpatsch/alacritty-change-color-scheme/alacritty-change-color-scheme.js
index 7d3d961d3928..ca2e86a2b798 100644
--- a/users/Profpatsch/alacritty-change-color-scheme/alacritty-change-color-scheme.js
+++ b/users/Profpatsch/alacritty-change-color-scheme/alacritty-change-color-scheme.js
@@ -1,7 +1,22 @@
+//@ts-check
+
 // Step 3: Create the script
 const dbus = require('dbus-native');
 const fs = require('fs');
 const assert = require('assert');
+const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
+const {
+  BasicTracerProvider,
+  BatchSpanProcessor,
+  ConsoleSpanExporter,
+  SimpleSpanProcessor,
+} = require('@opentelemetry/sdk-trace-base');
+const opentelemetry = require('@opentelemetry/api');
+const { hrTime } = require('@opentelemetry/core');
+const { AsyncHooksContextManager } = require('@opentelemetry/context-async-hooks');
+const { EventEmitter } = require('stream');
+const { setTimeout } = require('node:timers/promises');
+const { promisify } = require('util');
 
 // NB: this code is like 80% copilot generated, and seriously missing error handling.
 // It might break at any time, but for now it seems to work lol.
@@ -21,8 +36,148 @@ console.log(`Dark theme: ${darkTheme}`);
 console.log(`Light theme: ${lightTheme}`);
 
 // Connect to the user session bus
+/** @type any */
 const bus = dbus.sessionBus();
 
+opentelemetry.diag.setLogger({ ...console, verbose: console.log });
+
+const exporter = new OTLPTraceExporter();
+const consoleExporter = new ConsoleSpanExporter();
+const provider = new BasicTracerProvider({
+  //@ts-ignore
+  resource: {
+    attributes: {
+      'service.name': 'alacritty-change-color-scheme',
+      // 'service.namespace': 'default',
+      // 'service.instance.id': 'alacritty-change-color-scheme-01',
+      // 'service.version': '0.0.1',
+    },
+  },
+  spanProcessors: [
+    // new BatchSpanProcessor(exporter, {
+    //   maxQueueSize: 100,
+    //   scheduledDelayMillis: 5000,
+    // }),
+    new SimpleSpanProcessor(exporter),
+    new SimpleSpanProcessor(consoleExporter),
+  ],
+});
+provider.register({
+  contextManager: new AsyncHooksContextManager().enable(),
+});
+
+const dbusSpanEmitter = new EventEmitter();
+
+dbusSpanEmitter.on('new-root-span', onNewRootSpan);
+
+/** @typedef {{spanId: string, name: string, parentId?: string, startTime?: opentelemetry.TimeInput, attributes?: opentelemetry.Attributes}} StartSpan */
+/** @typedef {{spanId: string, endTime?: opentelemetry.TimeInput}} EndSpan */
+
+/** @param {opentelemetry.Tracer} tracer,
+ *  @param {StartSpan} spanData */
+function emitNewRootSpanEvent(tracer, spanData) {
+  dbusSpanEmitter.emit('new-root-span', tracer, spanData);
+}
+
+/** @param {StartSpan} childSpanData */
+function emitNewChildSpanEvent(parentSpanId, childSpanData) {
+  dbusSpanEmitter.emit(`new-child-span-for/${parentSpanId}`, childSpanData);
+}
+
+/** @param {EndSpan} endSpanData */
+function emitEndSpanEvent(endSpanData) {
+  dbusSpanEmitter.emit(`end-span-for/${endSpanData.spanId}`, endSpanData);
+}
+
+/** @param {opentelemetry.Tracer} tracer
+ *  @param {StartSpan} spanData */
+function onNewRootSpan(tracer, spanData) {
+  console.log(`New span: ${spanData.spanId}`);
+  setupActiveSpan(tracer, spanData);
+}
+
+/** @param {opentelemetry.Tracer} tracer
+ *  @param {StartSpan} spanData */
+function setupActiveSpan(tracer, spanData) {
+  const SPAN_TIMEOUT = 1_000_000; // 1000 seconds
+  const SPAN_TIMEOUT_SHORT = 10_000; // 10 seconds
+  if (typeof spanData.startTime === 'number') {
+    console.warn(
+      'startTime is a number, not a hrTime tuple. This would use perfomance.now() in the wrong context, so we are ignoring it.',
+    );
+    spanData.startTime = undefined;
+  }
+  tracer.startActiveSpan(
+    spanData.name,
+    { startTime: spanData.startTime, attributes: spanData.attributes },
+    span => {
+      let activeContext = opentelemetry.context.active();
+
+      /** @param {{spanId: string, name: string}} childSpanData */
+      function onNewChildSpan(childSpanData) {
+        opentelemetry.context.with(activeContext, () => {
+          console.log(`New child span: ${childSpanData.spanId}`);
+          setupActiveSpan(tracer, childSpanData);
+        });
+      }
+      dbusSpanEmitter.on(`new-child-span-for/${spanData.spanId}`, onNewChildSpan);
+
+      const removeTimeoutOnEnd = new AbortController();
+
+      /** @param {{endTime?: opentelemetry.TimeInput}} endSpanData */
+      function onEndSpan(endSpanData) {
+        opentelemetry.context.with(activeContext, () => {
+          console.log(`End span: ${spanData.spanId}`);
+          if (typeof endSpanData.endTime === 'number') {
+            console.warn(
+              'endTime is a number, not a hrTime tuple. This would use perfomance.now() in the wrong context, so we are ignoring it.',
+            );
+            endSpanData.endTime = undefined;
+          }
+          span.end(endSpanData.endTime);
+          // we don’t remove the child span listener here, because in theory child spans don’t have to be inside parent spans.
+          // However, we only want to keep the child span listener around for a little more, so let’s set a shorter timeout here
+          setTimeout(SPAN_TIMEOUT_SHORT).then(() => {
+            dbusSpanEmitter.off(`new-child-span-for/${spanData.spanId}`, onNewChildSpan);
+          });
+          // remove the general long timeout belove
+          removeTimeoutOnEnd.abort();
+        });
+      }
+      dbusSpanEmitter.once(`end-span-for/${spanData.spanId}`, onEndSpan);
+
+      // don't keep spans contexts open forever if we never get a second message
+      // but we don’t end the spans, just remove the listeners.
+      setTimeout(SPAN_TIMEOUT, undefined, { signal: removeTimeoutOnEnd.signal })
+        .then(() => {
+          console.warn(
+            `Timeout for span ${spanData.spanId}, removing all event listeners`,
+          );
+          dbusSpanEmitter.off(`new-child-span-for/${spanData.spanId}`, onNewChildSpan);
+          dbusSpanEmitter.off(`end-span-for/${spanData.spanId}`, onEndSpan);
+        })
+        .catch(err => {
+          if (err.name === 'AbortError') {
+            // console.log(`Timeout for span ${spanData.spanId} was aborted`);
+          } else {
+            throw err;
+          }
+        });
+    },
+  );
+}
+
+// tracer.startActiveSpan('222', span => {
+//   span.setAttribute('blabla', 'alacritty-change-color-scheme');
+//   tracer.startActiveSpan('333', span => {
+//     span.setAttribute('foo', 'bar');
+//     span.end();
+//   });
+//   // span.setAttribute('service.name', 'alacritty-change-color-scheme');
+//   // span.setAttribute('service.version', '0.0.1');
+//   span.end();
+// });
+
 // set XDG_CONFIG_HOME if it's not set
 if (!process.env.XDG_CONFIG_HOME) {
   process.env.XDG_CONFIG_HOME = process.env.HOME + '/.config';
@@ -35,8 +190,12 @@ function getThemePathSync(theme) {
   return absolutePath;
 }
 
-// write new color scheme
-function writeAlacrittyColorConfig(cs, theme) {
+/** write new color scheme
+ *
+ * @param {'prefer-dark' | 'prefer-light'} cs
+ */
+function writeAlacrittyColorConfig(cs) {
+  const theme = cs === 'prefer-dark' ? darkTheme : lightTheme;
   console.log(`Writing color scheme ${cs} with theme ${theme}`);
   fs.writeFileSync(
     process.env.XDG_CONFIG_HOME + '/alacritty/alacritty-colors-autogen.toml',
@@ -46,35 +205,41 @@ general.import = ["${theme}"]`,
   );
 }
 
-// get the current value of the color scheme from dbus
-function getColorScheme() {
-  return new Promise((resolve, reject) => {
-    bus
-      .getService('org.freedesktop.portal.Desktop')
-      .getInterface(
-        '/org/freedesktop/portal/desktop',
-        'org.freedesktop.portal.Settings',
-        (err, iface) => {
-          if (err) {
-            reject(`error getting interface: ${err}`);
-            return;
-          }
+/** get the current value of the color scheme from dbus
+ *
+ * @returns {Promise<'prefer-dark' | 'prefer-light'>}
+ */
+async function getColorScheme() {
+  let service = bus.getService('org.freedesktop.portal.Desktop');
+  let iface = await promisifyMethodAnnotate(
+    service,
+    service.getInterface,
+    'Error getting interface',
+    '/org/freedesktop/portal/desktop',
+    'org.freedesktop.portal.Settings',
+  );
 
-          iface.ReadOne(
-            'org.gnome.desktop.interface',
-            'color-scheme',
-            (err, [_, [value]]) => {
-              if (err) {
-                reject(err);
-                return;
-              }
-
-              resolve(value);
-            },
-          );
-        },
-      );
-  });
+  const [_, [value]] = await promisifyMethodAnnotate(
+    iface,
+    iface.ReadOne,
+    'Error reading color scheme',
+    'org.gnome.desktop.interface',
+    'color-scheme',
+  );
+  assert(value === 'prefer-dark' || value === 'prefer-light');
+  return value;
+}
+
+/** promisify an object method and annotate any errors that get thrown
+ * @template {Function} A
+ * @param {object} obj
+ * @param {A} method
+ * @param {string} msg
+ * @param  {...any} args
+ * @returns {Promise<ReturnType<A>>}
+ */
+function promisifyMethodAnnotate(obj, method, msg, ...args) {
+  return promisify(method.bind(obj))(...args).catch(annotateErr(msg));
 }
 
 /** write respective alacritty config if the colorscheme changes.
@@ -90,40 +255,33 @@ function writeAlacrittyColorConfigIfDifferent(cs) {
   previous = cs;
 
   console.log(`Color scheme changed to ${cs}`);
-  writeAlacrittyColorConfig(cs, cs === 'prefer-dark' ? darkTheme : lightTheme);
+  writeAlacrittyColorConfig(cs);
 }
 
 /** Listen on the freedesktop SettingChanged dbus interface for the color-scheme setting to change. */
-function listenForColorschemeChange() {
-  return new Promise((resolve, reject) => {
-    bus
-      .getService('org.freedesktop.portal.Desktop')
-      .getInterface(
-        '/org/freedesktop/portal/desktop',
-        'org.freedesktop.portal.Settings',
-        (err, iface) => {
-          if (err) {
-            reject(`error getting interface: ${err}`);
-          }
+async function listenForColorschemeChange() {
+  const service = bus.getService('org.freedesktop.portal.Desktop');
 
-          // Listen for SettingChanged signals
-          iface.on('SettingChanged', (interfaceName, key, [_, [newValue]]) => {
-            if (
-              interfaceName === 'org.gnome.desktop.interface' &&
-              key == 'color-scheme'
-            ) {
-              writeAlacrittyColorConfigIfDifferent(newValue);
-            }
-          });
+  const iface = await promisifyMethodAnnotate(
+    service,
+    service.getInterface,
+    'Error getting interface',
+    '/org/freedesktop/portal/desktop',
+    'org.freedesktop.portal.Settings',
+  );
 
-          console.log('Listening for color scheme changes...');
-        },
-      );
+  // Listen for SettingChanged signals
+  iface.on('SettingChanged', (interfaceName, key, [_, [newValue]]) => {
+    if (interfaceName === 'org.gnome.desktop.interface' && key == 'color-scheme') {
+      writeAlacrittyColorConfigIfDifferent(newValue);
+    }
   });
+
+  console.log('Listening for color scheme changes...');
 }
 
 /** Create a dbus service that binds against the interface de.profpatsch.alacritty.ColorScheme and implements the method SetColorScheme */
-function exportColorSchemeDbusInterface() {
+async function exportColorSchemeDbusInterface() {
   console.log('Exporting color scheme interface de.profpatsch.alacritty.ColorScheme');
   const ifaceName = 'de.profpatsch.alacritty.ColorScheme';
   const iface = {
@@ -141,18 +299,18 @@ function exportColorSchemeDbusInterface() {
   };
 
   try {
-    bus.requestName(ifaceName, 0, (err, retCode) => {
-      if (err) {
-        console.error(
-          'Error requesting name for interface de.profpatsch.alacritty.ColorScheme',
-        );
-        console.error(err);
-      }
-      console.log(
-        `Request name returned ${retCode} for interface de.profpatsch.alacritty.ColorScheme`,
-      );
-    });
+    const retCode = await promisifyMethodAnnotate(
+      bus,
+      bus.requestName,
+      'Error requesting name for interface de.profpatsch.alacritty.ColorScheme',
+      ifaceName,
+      0,
+    );
+    console.log(
+      `Request name returned ${retCode} for interface de.profpatsch.alacritty.ColorScheme`,
+    );
     bus.exportInterface(ifaceImpl, '/de/profpatsch/alacritty/ColorScheme', iface);
+    bus.exportInterface(ifaceImpl, '/de/profpatsch/alacritty/ColorScheme2', iface);
     console.log('Exported interface de.profpatsch.alacritty.ColorScheme');
   } catch (err) {
     console.log('Error exporting interface de.profpatsch.alacritty.ColorScheme');
@@ -160,10 +318,239 @@ function exportColorSchemeDbusInterface() {
   }
 }
 
+/** Annotate an error as a promise .catch handler (rethrows the annotated error)
+ * @param {string} msg
+ * @returns {function(Error): void}
+ */
+function annotateErr(msg) {
+  return err => {
+    msg = err.message ? `${msg}: ${err.message}` : msg;
+    err.message = msg;
+    throw err;
+  };
+}
+
+/** @type any */
+const bus2 = dbus.sessionBus();
+async function exportOtelInterface() {
+  console.log('Exporting OpenTelemetry interface');
+
+  try {
+    const retCode = await promisifyMethodAnnotate(
+      bus2,
+      bus2.requestName,
+      'Error requesting name for interface de.profpatsch.otel.Tracer',
+
+      'de.profpatsch.otel.Tracer',
+      0,
+    );
+    console.log(
+      `Request name returned ${retCode} for interface de.profpatsch.otel.Tracer`,
+    );
+
+    const traceIface = {
+      name: 'de.profpatsch.otel.Tracer',
+      methods: {
+        // These both just take a json string as input
+        StartSpan: ['s', ''],
+        EndSpan: ['s', ''],
+        // An array of [(isStartSpan: bool, span: Span)]
+        // So that you don’t have to call dbus for every span
+        BatchSpans: ['a(bs)', ''],
+      },
+    };
+    /** @type {(arg: {tracer: opentelemetry.Tracer, tracerName: string}) => {StartSpan: (input: string) => void, EndSpan: (input: string) => void, BatchSpans: (input: [boolean, string][]) => void }} */
+    const traceImpl = tracer => ({
+      StartSpan: function (input) {
+        // TODO: actually verify json input
+        /** @type {StartSpan} */
+        const spanArgs = JSON.parse(input);
+        if (spanArgs.parentId === undefined) {
+          console.log(
+            `Tracing root span ${spanArgs.name} with tracer ${tracer.tracerName}`,
+          );
+          emitNewRootSpanEvent(tracer.tracer, spanArgs);
+        } else {
+          console.log(
+            `Tracing child span ${spanArgs.name} with tracer ${tracer.tracerName}`,
+          );
+          emitNewChildSpanEvent(spanArgs.parentId, spanArgs);
+        }
+      },
+      EndSpan: function (input) {
+        // TODO: actually verify json input
+        /** @type {EndSpan} */
+        const spanArgs = JSON.parse(input);
+        console.log(`Ending span ${spanArgs.spanId} with tracer ${tracer.tracerName}`);
+        emitEndSpanEvent(spanArgs);
+      },
+      BatchSpans: function (input) {
+        // lol
+        for (const [isStartSpan, span] of input) {
+          if (isStartSpan) {
+            traceImpl(tracer).StartSpan(span);
+          } else {
+            traceImpl(tracer).EndSpan(span);
+          }
+        }
+      },
+    });
+    bus2.exportInterface(
+      {
+        CreateTracer: function (tracerName) {
+          console.log(`Creating tracer with name ${tracerName}`);
+          const tracer = opentelemetry.trace.getTracer(tracerName, '0.0.1');
+          bus2.exportInterface(
+            traceImpl({ tracer, tracerName }),
+            `/de/profpatsch/otel/tracers/${tracerName}`,
+            traceIface,
+          );
+          return `/de/profpatsch/otel/tracers/${tracerName}`;
+        },
+      },
+      '/de/profpatsch/otel/TracerFactory',
+      {
+        name: 'de.profpatsch.otel.TracerFactory',
+        methods: {
+          CreateTracer: ['s', 's'],
+        },
+      },
+    );
+    console.log('Exported interface de.profpatsch.otel.TracerFactory');
+  } catch (err) {
+    console.log('Error exporting interface de.profpatsch.alacritty.ColorScheme');
+    console.error(err);
+  }
+}
+
+/** Returns the callsite of the function calling `getParentCallsite`, if any. */
+async function getParentCallsite() {
+  const getCallsites = (await import('callsites')).default;
+  const cs = getCallsites();
+  return cs[2] ?? cs[1] ?? null;
+}
+
+/** Calls the tracer dbus interface, sets up a tracer
+ *
+ * @param {string} tracerName The name of the tracer to set up
+ * @returns {Promise<{
+ *   StartSpan: (spanData: StartSpan) => Promise<void>,
+ *   EndSpan: (spanData: EndSpan) => Promise<void>,
+ *   BatchSpans: (spans: ([true, StartSpan] | [false, EndSpan])[]) => Promise<void>
+ * }>}
+ */
+async function setupTracer(tracerName) {
+  const parentCallsite = await getParentCallsite();
+  console.log(`Setting up tracer ${tracerName} from ${parentCallsite?.getFileName()}`);
+  const service = bus2.getService('de.profpatsch.otel.Tracer');
+  const iface = await promisifyMethodAnnotate(
+    service,
+    service.getInterface,
+    'Error getting interface',
+    '/de/profpatsch/otel/TracerFactory',
+    'de.profpatsch.otel.TracerFactory',
+  );
+  const path = await promisifyMethodAnnotate(
+    iface,
+    iface.CreateTracer,
+    'Error creating tracer',
+    tracerName,
+  );
+  const tracerIface = await promisifyMethodAnnotate(
+    service,
+    service.getInterface,
+    'Error getting interface',
+    path,
+    'de.profpatsch.otel.Tracer',
+  );
+
+  function StartSpan(spanData) {
+    return promisifyMethodAnnotate(
+      tracerIface,
+      tracerIface.StartSpan,
+      'Error starting span',
+      JSON.stringify(spanData),
+    );
+  }
+  function EndSpan(spanData) {
+    return promisifyMethodAnnotate(
+      tracerIface,
+      tracerIface.EndSpan,
+      'Error ending span',
+      JSON.stringify(spanData),
+    );
+  }
+  function BatchSpans(spans) {
+    return promisifyMethodAnnotate(
+      tracerIface,
+      tracerIface.BatchSpans,
+      'Error batching spans',
+      spans.map(([isStartSpan, span]) => [isStartSpan, JSON.stringify(span)]),
+    );
+  }
+  return {
+    StartSpan,
+    EndSpan,
+    BatchSpans,
+  };
+}
+
 async function main() {
+  await exportOtelInterface();
+
+  const tracer = await setupTracer('hello');
+  await tracer.StartSpan({
+    name: 'hello',
+    spanId: 'hello',
+    startTime: hrTime(),
+    attributes: {
+      foo: 'bar',
+    },
+  });
+  await tracer.StartSpan({
+    name: 'world',
+    spanId: 'world',
+    parentId: 'hello',
+    attributes: {
+      bar: 'baz',
+    },
+  });
+  await tracer.EndSpan({
+    spanId: 'world',
+    endTime: hrTime(),
+  });
+  await tracer.EndSpan({
+    spanId: 'hello',
+    endTime: hrTime(),
+  });
+  const t = performance.now();
+  await tracer.BatchSpans([
+    [
+      true,
+      {
+        name: 'batchy',
+        spanId: 'batchy',
+        startTime: t,
+        attributes: { foo: 'bar' },
+      },
+    ],
+    [
+      true,
+      {
+        name: 'worldy',
+        spanId: 'worldy',
+        parentId: 'batchy',
+        startTime: t + 100,
+        attributes: { bar: 'baz' },
+      },
+    ],
+    [false, { spanId: 'worldy', endTime: t + 500 }],
+    [false, { spanId: 'batchy', endTime: t + 1000 }],
+  ]);
+
   // TODO: proper error handling, through proper callback promises for dbus function.
 
-  exportColorSchemeDbusInterface();
+  await exportColorSchemeDbusInterface();
 
   // get the current color scheme
   const currentColorScheme = await getColorScheme();
diff --git a/users/Profpatsch/alacritty-change-color-scheme/package-lock.json b/users/Profpatsch/alacritty-change-color-scheme/package-lock.json
index 38e2b437c432..19f75c42579b 100644
--- a/users/Profpatsch/alacritty-change-color-scheme/package-lock.json
+++ b/users/Profpatsch/alacritty-change-color-scheme/package-lock.json
@@ -9,7 +9,267 @@
       "version": "1.0.0",
       "license": "ISC",
       "dependencies": {
+        "@opentelemetry/api": "^1.9.0",
+        "@opentelemetry/context-async-hooks": "^1.29.0",
+        "@opentelemetry/core": "^1.29.0",
+        "@opentelemetry/exporter-trace-otlp-http": "^0.56.0",
+        "@opentelemetry/sdk-trace-base": "^1.29.0",
         "dbus-native": "^0.4.0"
+      },
+      "bin": {
+        "alacritty-change-color-scheme": "alacritty-change-color-scheme.js"
+      }
+    },
+    "node_modules/@opentelemetry/api": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
+      "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/@opentelemetry/api-logs": {
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.56.0.tgz",
+      "integrity": "sha512-Wr39+94UNNG3Ei9nv3pHd4AJ63gq5nSemMRpCd8fPwDL9rN3vK26lzxfH27mw16XzOSO+TpyQwBAMaLxaPWG0g==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/context-async-hooks": {
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.29.0.tgz",
+      "integrity": "sha512-TKT91jcFXgHyIDF1lgJF3BHGIakn6x0Xp7Tq3zoS3TMPzT9IlP0xEavWP8C1zGjU9UmZP2VR1tJhW9Az1A3w8Q==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/core": {
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.29.0.tgz",
+      "integrity": "sha512-gmT7vAreXl0DTHD2rVZcw3+l2g84+5XiHIqdBUxXbExymPCvSsGOpiwMmn8nkiJur28STV31wnhIDrzWDPzjfA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/semantic-conventions": "1.28.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/exporter-trace-otlp-http": {
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.56.0.tgz",
+      "integrity": "sha512-vqVuJvcwameA0r0cNrRzrZqPLB0otS+95g0XkZdiKOXUo81wYdY6r4kyrwz4nSChqTBEFm0lqi/H2OWGboOa6g==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/otlp-exporter-base": "0.56.0",
+        "@opentelemetry/otlp-transformer": "0.56.0",
+        "@opentelemetry/resources": "1.29.0",
+        "@opentelemetry/sdk-trace-base": "1.29.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/otlp-exporter-base": {
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.56.0.tgz",
+      "integrity": "sha512-eURvv0fcmBE+KE1McUeRo+u0n18ZnUeSc7lDlW/dzlqFYasEbsztTK4v0Qf8C4vEY+aMTjPKUxBG0NX2Te3Pmw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/otlp-transformer": "0.56.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/otlp-transformer": {
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.56.0.tgz",
+      "integrity": "sha512-kVkH/W2W7EpgWWpyU5VnnjIdSD7Y7FljQYObAQSKdRcejiwMj2glypZtUdfq1LTJcv4ht0jyTrw1D3CCxssNtQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/api-logs": "0.56.0",
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/resources": "1.29.0",
+        "@opentelemetry/sdk-logs": "0.56.0",
+        "@opentelemetry/sdk-metrics": "1.29.0",
+        "@opentelemetry/sdk-trace-base": "1.29.0",
+        "protobufjs": "^7.3.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/resources": {
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.29.0.tgz",
+      "integrity": "sha512-s7mLXuHZE7RQr1wwweGcaRp3Q4UJJ0wazeGlc/N5/XSe6UyXfsh1UQGMADYeg7YwD+cEdMtU1yJAUXdnFzYzyQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/semantic-conventions": "1.28.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/sdk-logs": {
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.56.0.tgz",
+      "integrity": "sha512-OS0WPBJF++R/cSl+terUjQH5PebloidB1Jbbecgg2rnCmQbTST9xsRes23bLfDQVRvmegmHqDh884h0aRdJyLw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/api-logs": "0.56.0",
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/resources": "1.29.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.4.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/sdk-metrics": {
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.29.0.tgz",
+      "integrity": "sha512-MkVtuzDjXZaUJSuJlHn6BSXjcQlMvHcsDV7LjY4P6AJeffMa4+kIGDjzsCf6DkAh6Vqlwag5EWEam3KZOX5Drw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/resources": "1.29.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.3.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/sdk-trace-base": {
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.29.0.tgz",
+      "integrity": "sha512-hEOpAYLKXF3wGJpXOtWsxEtqBgde0SCv+w+jvr3/UusR4ll3QrENEGnSl1WDCyRrpqOQ5NCNOvZch9UFVa7MnQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.29.0",
+        "@opentelemetry/resources": "1.29.0",
+        "@opentelemetry/semantic-conventions": "1.28.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+      "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@protobufjs/aspromise": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+      "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/base64": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+      "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/codegen": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+      "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/eventemitter": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+      "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/fetch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+      "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@protobufjs/aspromise": "^1.1.1",
+        "@protobufjs/inquire": "^1.1.0"
+      }
+    },
+    "node_modules/@protobufjs/float": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+      "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/inquire": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+      "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/path": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+      "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/pool": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+      "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@protobufjs/utf8": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+      "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@types/node": {
+      "version": "22.10.1",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz",
+      "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.20.0"
       }
     },
     "node_modules/abstract-socket": {
@@ -152,6 +412,36 @@
         "through": "~2.3"
       }
     },
+    "node_modules/protobufjs": {
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
+      "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
+      "hasInstallScript": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@protobufjs/aspromise": "^1.1.2",
+        "@protobufjs/base64": "^1.1.2",
+        "@protobufjs/codegen": "^2.0.4",
+        "@protobufjs/eventemitter": "^1.1.0",
+        "@protobufjs/fetch": "^1.1.0",
+        "@protobufjs/float": "^1.0.2",
+        "@protobufjs/inquire": "^1.1.0",
+        "@protobufjs/path": "^1.1.2",
+        "@protobufjs/pool": "^1.1.0",
+        "@protobufjs/utf8": "^1.1.0",
+        "@types/node": ">=13.7.0",
+        "long": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/protobufjs/node_modules/long": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
+      "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
+      "license": "Apache-2.0"
+    },
     "node_modules/put": {
       "version": "0.0.6",
       "resolved": "https://registry.npmjs.org/put/-/put-0.0.6.tgz",
@@ -215,6 +505,12 @@
       "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
       "license": "MIT"
     },
+    "node_modules/undici-types": {
+      "version": "6.20.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+      "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+      "license": "MIT"
+    },
     "node_modules/wordwrap": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
diff --git a/users/Profpatsch/alacritty-change-color-scheme/package.json b/users/Profpatsch/alacritty-change-color-scheme/package.json
index 720ee4b15657..e1b4af0c01b2 100644
--- a/users/Profpatsch/alacritty-change-color-scheme/package.json
+++ b/users/Profpatsch/alacritty-change-color-scheme/package.json
@@ -10,6 +10,11 @@
   "license": "ISC",
   "description": "",
   "dependencies": {
+    "@opentelemetry/api": "^1.9.0",
+    "@opentelemetry/context-async-hooks": "^1.29.0",
+    "@opentelemetry/core": "^1.29.0",
+    "@opentelemetry/exporter-trace-otlp-http": "^0.56.0",
+    "@opentelemetry/sdk-trace-base": "^1.29.0",
     "dbus-native": "^0.4.0"
   }
 }