- Add full Telegram bot functionality with Z.AI API integration
- Implement 4 tools: Bash, FileEdit, WebSearch, Git
- Add 3 agents: Code Reviewer, Architect, DevOps Engineer
- Add 6 skills for common coding tasks
- Add systemd service file for 24/7 operation
- Add nginx configuration for HTTPS webhook
- Add comprehensive documentation
- Implement WebSocket server for real-time updates
- Add logging system with Winston
- Add environment validation
🤖 zCode CLI X - Agentic coder with Z.AI + Telegram integration
384 lines
14 KiB
JavaScript
384 lines
14 KiB
JavaScript
/*
|
|
* Copyright The OpenTelemetry Authors
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
import { diag, SpanStatusCode } from '@opentelemetry/api';
|
|
import { addHrTimes, millisToHrTime, hrTime, hrTimeDuration, isAttributeValue, isTimeInput, isTimeInputHrTime, otperformance, sanitizeAttributes, } from '@opentelemetry/core';
|
|
import { ATTR_EXCEPTION_MESSAGE, ATTR_EXCEPTION_STACKTRACE, ATTR_EXCEPTION_TYPE, } from '@opentelemetry/semantic-conventions';
|
|
import { ExceptionEventName } from './enums';
|
|
/**
|
|
* This class represents a span.
|
|
*/
|
|
export class SpanImpl {
|
|
// Below properties are included to implement ReadableSpan for export
|
|
// purposes but are not intended to be written-to directly.
|
|
_spanContext;
|
|
kind;
|
|
parentSpanContext;
|
|
attributes = {};
|
|
links = [];
|
|
events = [];
|
|
startTime;
|
|
resource;
|
|
instrumentationScope;
|
|
_droppedAttributesCount = 0;
|
|
_droppedEventsCount = 0;
|
|
_droppedLinksCount = 0;
|
|
_attributesCount = 0;
|
|
name;
|
|
status = {
|
|
code: SpanStatusCode.UNSET,
|
|
};
|
|
endTime = [0, 0];
|
|
_ended = false;
|
|
_duration = [-1, -1];
|
|
_spanProcessor;
|
|
_spanLimits;
|
|
_attributeValueLengthLimit;
|
|
_recordEndMetrics;
|
|
_performanceStartTime;
|
|
_performanceOffset;
|
|
_startTimeProvided;
|
|
/**
|
|
* Constructs a new SpanImpl instance.
|
|
*/
|
|
constructor(opts) {
|
|
const now = Date.now();
|
|
this._spanContext = opts.spanContext;
|
|
this._performanceStartTime = otperformance.now();
|
|
this._performanceOffset =
|
|
now - (this._performanceStartTime + otperformance.timeOrigin);
|
|
this._startTimeProvided = opts.startTime != null;
|
|
this._spanLimits = opts.spanLimits;
|
|
this._attributeValueLengthLimit =
|
|
this._spanLimits.attributeValueLengthLimit ?? 0;
|
|
this._spanProcessor = opts.spanProcessor;
|
|
this.name = opts.name;
|
|
this.parentSpanContext = opts.parentSpanContext;
|
|
this.kind = opts.kind;
|
|
if (opts.links) {
|
|
for (const link of opts.links) {
|
|
this.addLink(link);
|
|
}
|
|
}
|
|
this.startTime = this._getTime(opts.startTime ?? now);
|
|
this.resource = opts.resource;
|
|
this.instrumentationScope = opts.scope;
|
|
this._recordEndMetrics = opts.recordEndMetrics;
|
|
if (opts.attributes != null) {
|
|
this.setAttributes(opts.attributes);
|
|
}
|
|
this._spanProcessor.onStart(this, opts.context);
|
|
}
|
|
spanContext() {
|
|
return this._spanContext;
|
|
}
|
|
setAttribute(key, value) {
|
|
if (value == null || this._isSpanEnded())
|
|
return this;
|
|
if (key.length === 0) {
|
|
diag.warn(`Invalid attribute key: ${key}`);
|
|
return this;
|
|
}
|
|
if (!isAttributeValue(value)) {
|
|
diag.warn(`Invalid attribute value set for key: ${key}`);
|
|
return this;
|
|
}
|
|
const { attributeCountLimit } = this._spanLimits;
|
|
const isNewKey = !Object.prototype.hasOwnProperty.call(this.attributes, key);
|
|
if (attributeCountLimit !== undefined &&
|
|
this._attributesCount >= attributeCountLimit &&
|
|
isNewKey) {
|
|
this._droppedAttributesCount++;
|
|
return this;
|
|
}
|
|
this.attributes[key] = this._truncateToSize(value);
|
|
if (isNewKey) {
|
|
this._attributesCount++;
|
|
}
|
|
return this;
|
|
}
|
|
setAttributes(attributes) {
|
|
for (const key in attributes) {
|
|
if (Object.prototype.hasOwnProperty.call(attributes, key)) {
|
|
this.setAttribute(key, attributes[key]);
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
*
|
|
* @param name Span Name
|
|
* @param [attributesOrStartTime] Span attributes or start time
|
|
* if type is {@type TimeInput} and 3rd param is undefined
|
|
* @param [timeStamp] Specified time stamp for the event
|
|
*/
|
|
addEvent(name, attributesOrStartTime, timeStamp) {
|
|
if (this._isSpanEnded())
|
|
return this;
|
|
const { eventCountLimit } = this._spanLimits;
|
|
if (eventCountLimit === 0) {
|
|
diag.warn('No events allowed.');
|
|
this._droppedEventsCount++;
|
|
return this;
|
|
}
|
|
if (eventCountLimit !== undefined &&
|
|
this.events.length >= eventCountLimit) {
|
|
if (this._droppedEventsCount === 0) {
|
|
diag.debug('Dropping extra events.');
|
|
}
|
|
this.events.shift();
|
|
this._droppedEventsCount++;
|
|
}
|
|
if (isTimeInput(attributesOrStartTime)) {
|
|
if (!isTimeInput(timeStamp)) {
|
|
timeStamp = attributesOrStartTime;
|
|
}
|
|
attributesOrStartTime = undefined;
|
|
}
|
|
const sanitized = sanitizeAttributes(attributesOrStartTime);
|
|
const { attributePerEventCountLimit } = this._spanLimits;
|
|
const attributes = {};
|
|
let droppedAttributesCount = 0;
|
|
let eventAttributesCount = 0;
|
|
for (const attr in sanitized) {
|
|
if (!Object.prototype.hasOwnProperty.call(sanitized, attr)) {
|
|
continue;
|
|
}
|
|
const attrVal = sanitized[attr];
|
|
if (attributePerEventCountLimit !== undefined &&
|
|
eventAttributesCount >= attributePerEventCountLimit) {
|
|
droppedAttributesCount++;
|
|
continue;
|
|
}
|
|
attributes[attr] = this._truncateToSize(attrVal);
|
|
eventAttributesCount++;
|
|
}
|
|
this.events.push({
|
|
name,
|
|
attributes,
|
|
time: this._getTime(timeStamp),
|
|
droppedAttributesCount,
|
|
});
|
|
return this;
|
|
}
|
|
addLink(link) {
|
|
if (this._isSpanEnded())
|
|
return this;
|
|
const { linkCountLimit } = this._spanLimits;
|
|
if (linkCountLimit === 0) {
|
|
this._droppedLinksCount++;
|
|
return this;
|
|
}
|
|
if (linkCountLimit !== undefined && this.links.length >= linkCountLimit) {
|
|
if (this._droppedLinksCount === 0) {
|
|
diag.debug('Dropping extra links.');
|
|
}
|
|
this.links.shift();
|
|
this._droppedLinksCount++;
|
|
}
|
|
const { attributePerLinkCountLimit } = this._spanLimits;
|
|
const sanitized = sanitizeAttributes(link.attributes);
|
|
const attributes = {};
|
|
let droppedAttributesCount = 0;
|
|
let linkAttributesCount = 0;
|
|
for (const attr in sanitized) {
|
|
if (!Object.prototype.hasOwnProperty.call(sanitized, attr)) {
|
|
continue;
|
|
}
|
|
const attrVal = sanitized[attr];
|
|
if (attributePerLinkCountLimit !== undefined &&
|
|
linkAttributesCount >= attributePerLinkCountLimit) {
|
|
droppedAttributesCount++;
|
|
continue;
|
|
}
|
|
attributes[attr] = this._truncateToSize(attrVal);
|
|
linkAttributesCount++;
|
|
}
|
|
const processedLink = { context: link.context };
|
|
if (linkAttributesCount > 0) {
|
|
processedLink.attributes = attributes;
|
|
}
|
|
if (droppedAttributesCount > 0) {
|
|
processedLink.droppedAttributesCount = droppedAttributesCount;
|
|
}
|
|
this.links.push(processedLink);
|
|
return this;
|
|
}
|
|
addLinks(links) {
|
|
for (const link of links) {
|
|
this.addLink(link);
|
|
}
|
|
return this;
|
|
}
|
|
setStatus(status) {
|
|
if (this._isSpanEnded())
|
|
return this;
|
|
if (status.code === SpanStatusCode.UNSET)
|
|
return this;
|
|
if (this.status.code === SpanStatusCode.OK)
|
|
return this;
|
|
const newStatus = { code: status.code };
|
|
// When using try-catch, the caught "error" is of type `any`. When then assigning `any` to `status.message`,
|
|
// TypeScript will not error. While this can happen during use of any API, it is more common on Span#setStatus()
|
|
// as it's likely used in a catch-block. Therefore, we validate if `status.message` is actually a string, null, or
|
|
// undefined to avoid an incorrect type causing issues downstream.
|
|
if (status.code === SpanStatusCode.ERROR) {
|
|
if (typeof status.message === 'string') {
|
|
newStatus.message = status.message;
|
|
}
|
|
else if (status.message != null) {
|
|
diag.warn(`Dropping invalid status.message of type '${typeof status.message}', expected 'string'`);
|
|
}
|
|
}
|
|
this.status = newStatus;
|
|
return this;
|
|
}
|
|
updateName(name) {
|
|
if (this._isSpanEnded())
|
|
return this;
|
|
this.name = name;
|
|
return this;
|
|
}
|
|
end(endTime) {
|
|
if (this._isSpanEnded()) {
|
|
diag.error(`${this.name} ${this._spanContext.traceId}-${this._spanContext.spanId} - You can only call end() on a span once.`);
|
|
return;
|
|
}
|
|
this.endTime = this._getTime(endTime);
|
|
this._duration = hrTimeDuration(this.startTime, this.endTime);
|
|
if (this._duration[0] < 0) {
|
|
diag.warn('Inconsistent start and end time, startTime > endTime. Setting span duration to 0ms.', this.startTime, this.endTime);
|
|
this.endTime = this.startTime.slice();
|
|
this._duration = [0, 0];
|
|
}
|
|
if (this._droppedEventsCount > 0) {
|
|
diag.warn(`Dropped ${this._droppedEventsCount} events because eventCountLimit reached`);
|
|
}
|
|
if (this._droppedLinksCount > 0) {
|
|
diag.warn(`Dropped ${this._droppedLinksCount} links because linkCountLimit reached`);
|
|
}
|
|
if (this._spanProcessor.onEnding) {
|
|
this._spanProcessor.onEnding(this);
|
|
}
|
|
this._recordEndMetrics?.();
|
|
this._ended = true;
|
|
this._spanProcessor.onEnd(this);
|
|
}
|
|
_getTime(inp) {
|
|
if (typeof inp === 'number' && inp <= otperformance.now()) {
|
|
// must be a performance timestamp
|
|
// apply correction and convert to hrtime
|
|
return hrTime(inp + this._performanceOffset);
|
|
}
|
|
if (typeof inp === 'number') {
|
|
return millisToHrTime(inp);
|
|
}
|
|
if (inp instanceof Date) {
|
|
return millisToHrTime(inp.getTime());
|
|
}
|
|
if (isTimeInputHrTime(inp)) {
|
|
return inp;
|
|
}
|
|
if (this._startTimeProvided) {
|
|
// if user provided a time for the start manually
|
|
// we can't use duration to calculate event/end times
|
|
return millisToHrTime(Date.now());
|
|
}
|
|
const msDuration = otperformance.now() - this._performanceStartTime;
|
|
return addHrTimes(this.startTime, millisToHrTime(msDuration));
|
|
}
|
|
isRecording() {
|
|
return this._ended === false;
|
|
}
|
|
recordException(exception, time) {
|
|
const attributes = {};
|
|
if (typeof exception === 'string') {
|
|
attributes[ATTR_EXCEPTION_MESSAGE] = exception;
|
|
}
|
|
else if (exception) {
|
|
if (exception.code) {
|
|
attributes[ATTR_EXCEPTION_TYPE] = exception.code.toString();
|
|
}
|
|
else if (exception.name) {
|
|
attributes[ATTR_EXCEPTION_TYPE] = exception.name;
|
|
}
|
|
if (exception.message) {
|
|
attributes[ATTR_EXCEPTION_MESSAGE] = exception.message;
|
|
}
|
|
if (exception.stack) {
|
|
attributes[ATTR_EXCEPTION_STACKTRACE] = exception.stack;
|
|
}
|
|
}
|
|
// these are minimum requirements from spec
|
|
if (attributes[ATTR_EXCEPTION_TYPE] || attributes[ATTR_EXCEPTION_MESSAGE]) {
|
|
this.addEvent(ExceptionEventName, attributes, time);
|
|
}
|
|
else {
|
|
diag.warn(`Failed to record an exception ${exception}`);
|
|
}
|
|
}
|
|
get duration() {
|
|
return this._duration;
|
|
}
|
|
get ended() {
|
|
return this._ended;
|
|
}
|
|
get droppedAttributesCount() {
|
|
return this._droppedAttributesCount;
|
|
}
|
|
get droppedEventsCount() {
|
|
return this._droppedEventsCount;
|
|
}
|
|
get droppedLinksCount() {
|
|
return this._droppedLinksCount;
|
|
}
|
|
_isSpanEnded() {
|
|
if (this._ended) {
|
|
const error = new Error(`Operation attempted on ended Span {traceId: ${this._spanContext.traceId}, spanId: ${this._spanContext.spanId}}`);
|
|
diag.warn(`Cannot execute the operation on ended Span {traceId: ${this._spanContext.traceId}, spanId: ${this._spanContext.spanId}}`, error);
|
|
}
|
|
return this._ended;
|
|
}
|
|
// Utility function to truncate given value within size
|
|
// for value type of string, will truncate to given limit
|
|
// for type of non-string, will return same value
|
|
_truncateToLimitUtil(value, limit) {
|
|
if (value.length <= limit) {
|
|
return value;
|
|
}
|
|
return value.substring(0, limit);
|
|
}
|
|
/**
|
|
* If the given attribute value is of type string and has more characters than given {@code attributeValueLengthLimit} then
|
|
* return string with truncated to {@code attributeValueLengthLimit} characters
|
|
*
|
|
* If the given attribute value is array of strings then
|
|
* return new array of strings with each element truncated to {@code attributeValueLengthLimit} characters
|
|
*
|
|
* Otherwise return same Attribute {@code value}
|
|
*
|
|
* @param value Attribute value
|
|
* @returns truncated attribute value if required, otherwise same value
|
|
*/
|
|
_truncateToSize(value) {
|
|
const limit = this._attributeValueLengthLimit;
|
|
// Check limit
|
|
if (limit <= 0) {
|
|
// Negative values are invalid, so do not truncate
|
|
diag.warn(`Attribute value limit must be positive, got ${limit}`);
|
|
return value;
|
|
}
|
|
// String
|
|
if (typeof value === 'string') {
|
|
return this._truncateToLimitUtil(value, limit);
|
|
}
|
|
// Array of strings
|
|
if (Array.isArray(value)) {
|
|
return value.map(val => typeof val === 'string' ? this._truncateToLimitUtil(val, limit) : val);
|
|
}
|
|
// Other types, no need to apply value length limit
|
|
return value;
|
|
}
|
|
}
|
|
//# sourceMappingURL=Span.js.map
|