refactor: Update startActiveSpan method in Telemetry class

This commit is contained in:
Simon Larsen 2024-08-27 14:51:38 +01:00
parent 4563f42794
commit fb8b00d383
No known key found for this signature in database
GPG Key ID: 96C5DCA24769DBCA
6 changed files with 338 additions and 338 deletions

View File

@ -39,27 +39,28 @@ RunCron(
timeoutInMS: OneUptimeDate.convertMinutesToMilliseconds(5),
},
async () => {
const span: Span = Telemetry.startSpan({
return await Telemetry.startActiveSpan<Promise<void>>({
name: "StatusPageCerts:OrderSSL",
attributes: {
jobName: "StatusPageCerts:OrderSSL",
options: {
attributes: {
schedule: IsDevelopment ? EVERY_FIFTEEN_MINUTE : EVERY_FIFTEEN_MINUTE,
runOnStartup: true,
timeoutInMS: OneUptimeDate.convertMinutesToMilliseconds(5),
},
},
fn: async (span: Span): Promise<void> => {
try {
await StatusPageDomainService.orderSSLForDomainsWhichAreNotOrderedYet();
Telemetry.endSpan(span);
} catch (err) {
Telemetry.recordExceptionMarkSpanAsErrorAndEndSpan({
span,
exception: err,
});
throw err;
}
},
});
try {
await StatusPageDomainService.orderSSLForDomainsWhichAreNotOrderedYet();
Telemetry.endSpan(span);
} catch (err) {
logger.error(err);
Telemetry.recordExceptionMarkSpanAsErrorAndEndSpan({
span,
exception: err,
});
throw err;
}
},
);

View File

@ -27,57 +27,60 @@ const RunCron: RunCronFunction = (
},
runFunction: PromiseVoidFunction,
): void => {
const span: Span = Telemetry.startSpan({
return Telemetry.startActiveSpan<void>({
name: "RunCron",
attributes: {
jobName: jobName,
"options.schedule": options.schedule,
"options.runOnStartup": options.runOnStartup,
options: {
attributes: {
jobName: jobName,
"options.schedule": options.schedule,
"options.runOnStartup": options.runOnStartup,
},
},
fn: (span: Span): void => {
try {
JobDictionary.setJobFunction(jobName, runFunction);
if (options.timeoutInMS) {
JobDictionary.setTimeoutInMs(jobName, options.timeoutInMS);
}
logger.debug("Adding job to the queue: " + jobName);
Queue.addJob(
QueueName.Worker,
jobName,
jobName,
{},
{
scheduleAt: options.schedule,
},
).catch((err: Error) => {
return logger.error(err);
});
if (options.runOnStartup) {
Queue.addJob(QueueName.Worker, jobName, jobName, {}, {}).catch(
(err: Error) => {
return logger.error(err);
},
);
}
} catch (err) {
// log this error
logger.error(err);
// record exception
span.recordException(err as SpanException);
// set span status
span.setStatus({
code: SpanStatusCode.ERROR,
});
} finally {
span.end();
}
},
});
try {
JobDictionary.setJobFunction(jobName, runFunction);
if (options.timeoutInMS) {
JobDictionary.setTimeoutInMs(jobName, options.timeoutInMS);
}
logger.debug("Adding job to the queue: " + jobName);
Queue.addJob(
QueueName.Worker,
jobName,
jobName,
{},
{
scheduleAt: options.schedule,
},
).catch((err: Error) => {
return logger.error(err);
});
if (options.runOnStartup) {
Queue.addJob(QueueName.Worker, jobName, jobName, {}, {}).catch(
(err: Error) => {
return logger.error(err);
},
);
}
} catch (err) {
// log this error
logger.error(err);
// record exception
span.recordException(err as SpanException);
// set span status
span.setStatus({
code: SpanStatusCode.ERROR,
});
} finally {
span.end();
}
};
export default RunCron;

View File

@ -6,7 +6,7 @@ import Express, {
} from "../Utils/Express";
import logger from "../Utils/Logger";
import Response from "../Utils/Response";
import Telemetry, { Span, SpanStatusCode } from "../Utils/Telemetry";
import Telemetry from "../Utils/Telemetry";
import Exception from "Common/Types/Exception/Exception";
import ServerException from "Common/Types/Exception/ServerException";
@ -92,13 +92,6 @@ export default class StatusAPI {
router.get(
"/status/live",
async (req: ExpressRequest, res: ExpressResponse) => {
const span: Span = Telemetry.startSpan({
name: "status.live",
attributes: {
status: "live",
},
});
try {
logger.debug("Live check");
await options.readyCheck();
@ -109,15 +102,6 @@ export default class StatusAPI {
status: "ok",
});
} catch (e) {
// record exception
span.recordException(e as Exception);
// set span status
span.setStatus({
code: SpanStatusCode.ERROR,
message: "Live check failed",
});
this.stausLiveFailed.add(1);
Response.sendErrorResponse(
req,
@ -126,8 +110,6 @@ export default class StatusAPI {
? e
: new ServerException("Server is not ready"),
);
} finally {
span.end();
}
},
);

View File

@ -87,70 +87,73 @@ export class Service extends DatabaseService<StatusPageDomain> {
}
public async orderCert(statusPageDomain: StatusPageDomain): Promise<void> {
const span: Span = Telemetry.startSpan({
return Telemetry.startActiveSpan<Promise<void>>({
name: "StatusPageDomainService.orderCert",
attributes: {
fullDomain: statusPageDomain.fullDomain,
_id: statusPageDomain.id?.toString(),
options: {
attributes: {
fullDomain: statusPageDomain.fullDomain,
_id: statusPageDomain.id?.toString(),
},
},
});
fn: async (span: Span): Promise<void> => {
try {
if (!statusPageDomain.fullDomain) {
const fetchedStatusPageDomain: StatusPageDomain | null =
await this.findOneBy({
query: {
_id: statusPageDomain.id!.toString(),
},
select: {
_id: true,
fullDomain: true,
},
props: {
isRoot: true,
},
});
try {
if (!statusPageDomain.fullDomain) {
const fetchedStatusPageDomain: StatusPageDomain | null =
await this.findOneBy({
query: {
_id: statusPageDomain.id!.toString(),
if (!fetchedStatusPageDomain) {
throw new BadDataException("Domain not found");
}
statusPageDomain = fetchedStatusPageDomain;
}
if (!statusPageDomain.fullDomain) {
throw new BadDataException(
"Unable to order certificate because domain is null",
);
}
await GreenlockUtil.orderCert({
domain: statusPageDomain.fullDomain as string,
validateCname: async (fullDomain: string) => {
return await this.isCnameValid(fullDomain);
},
select: {
_id: true,
fullDomain: true,
});
// update the order.
await this.updateOneById({
id: statusPageDomain.id!,
data: {
isSslOrdered: true,
},
props: {
isRoot: true,
},
});
if (!fetchedStatusPageDomain) {
throw new BadDataException("Domain not found");
Telemetry.endSpan(span);
} catch (err) {
Telemetry.recordExceptionMarkSpanAsErrorAndEndSpan({
span,
exception: err,
});
throw err;
}
statusPageDomain = fetchedStatusPageDomain;
}
if (!statusPageDomain.fullDomain) {
throw new BadDataException(
"Unable to order certificate because domain is null",
);
}
await GreenlockUtil.orderCert({
domain: statusPageDomain.fullDomain as string,
validateCname: async (fullDomain: string) => {
return await this.isCnameValid(fullDomain);
},
});
// update the order.
await this.updateOneById({
id: statusPageDomain.id!,
data: {
isSslOrdered: true,
},
props: {
isRoot: true,
},
});
Telemetry.endSpan(span);
} catch (err) {
Telemetry.recordExceptionMarkSpanAsErrorAndEndSpan({
span,
exception: err,
});
throw err;
}
},
});
}
public async updateSslProvisioningStatusForAllDomains(): Promise<void> {
@ -394,44 +397,45 @@ export class Service extends DatabaseService<StatusPageDomain> {
}
public async orderSSLForDomainsWhichAreNotOrderedYet(): Promise<void> {
const span: Span = Telemetry.startSpan({
return Telemetry.startActiveSpan<Promise<void>>({
name: "StatusPageDomainService.orderSSLForDomainsWhichAreNotOrderedYet",
attributes: {},
});
try {
const domains: Array<StatusPageDomain> = await this.findBy({
query: {
isSslOrdered: false,
},
select: {
_id: true,
fullDomain: true,
},
limit: LIMIT_MAX,
skip: 0,
props: {
isRoot: true,
},
});
for (const domain of domains) {
options: { attributes: {} },
fn: async (span: Span): Promise<void> => {
try {
await this.orderCert(domain);
} catch (e) {
logger.error(e);
const domains: Array<StatusPageDomain> = await this.findBy({
query: {
isSslOrdered: false,
},
select: {
_id: true,
fullDomain: true,
},
limit: LIMIT_MAX,
skip: 0,
props: {
isRoot: true,
},
});
for (const domain of domains) {
try {
await this.orderCert(domain);
} catch (e) {
logger.error(e);
}
}
Telemetry.endSpan(span);
} catch (err) {
Telemetry.recordExceptionMarkSpanAsErrorAndEndSpan({
span,
exception: err,
});
throw err;
}
}
Telemetry.endSpan(span);
} catch (err) {
Telemetry.recordExceptionMarkSpanAsErrorAndEndSpan({
span,
exception: err,
});
throw err;
}
},
});
}
public async verifyCnameWhoseCnameisNotVerified(): Promise<void> {

View File

@ -85,208 +85,217 @@ export default class GreenlockUtil {
}
public static async removeDomain(domain: string): Promise<void> {
const span: Span = Telemetry.startSpan({
name: "GreenlockUtil.removeDomain",
attributes: {
domain: domain,
},
});
try {
// remove certificate for this domain.
await AcmeCertificateService.deleteBy({
query: {
return await Telemetry.startActiveSpan<Promise<void>>({
name: "GreenlockUtil.orderCert",
options: {
attributes: {
domain: domain,
},
limit: 1,
skip: 0,
props: {
isRoot: true,
},
});
},
fn: async (span: Span): Promise<void> => {
try {
// remove certificate for this domain.
await AcmeCertificateService.deleteBy({
query: {
domain: domain,
},
limit: 1,
skip: 0,
props: {
isRoot: true,
},
});
Telemetry.endSpan(span);
} catch (err) {
logger.error(`Error removing domain: ${domain}`);
Telemetry.endSpan(span);
} catch (err) {
logger.error(`Error removing domain: ${domain}`);
Telemetry.recordExceptionMarkSpanAsErrorAndEndSpan({
span,
exception: err,
});
Telemetry.recordExceptionMarkSpanAsErrorAndEndSpan({
span,
exception: err,
});
throw err;
}
throw err;
}
},
});
}
public static async orderCert(data: {
domain: string;
validateCname: (domain: string) => Promise<boolean>;
}): Promise<void> {
const span: Span = Telemetry.startSpan({
return await Telemetry.startActiveSpan<Promise<void>>({
name: "GreenlockUtil.orderCert",
attributes: {
domain: data.domain,
options: {
attributes: {
domain: data.domain,
},
},
});
fn: async (span: Span): Promise<void> => {
try {
let { domain } = data;
try {
let { domain } = data;
domain = domain.trim().toLowerCase();
domain = domain.trim().toLowerCase();
const acmeAccountKeyInBase64: string = LetsEncryptAccountKey;
const acmeAccountKeyInBase64: string = LetsEncryptAccountKey;
if (!acmeAccountKeyInBase64) {
throw new ServerException(
"No lets encrypt account key found in environment variables. Please add one.",
);
}
if (!acmeAccountKeyInBase64) {
throw new ServerException(
"No lets encrypt account key found in environment variables. Please add one.",
);
}
let acmeAccountKey: string = Buffer.from(
acmeAccountKeyInBase64,
"base64",
).toString();
let acmeAccountKey: string = Buffer.from(
acmeAccountKeyInBase64,
"base64",
).toString();
acmeAccountKey = Text.replaceAll(acmeAccountKey, "\\n", "\n");
acmeAccountKey = Text.replaceAll(acmeAccountKey, "\\n", "\n");
//validate cname
//validate cname
const isValidCname: boolean = await data.validateCname(domain);
const isValidCname: boolean = await data.validateCname(domain);
if (!isValidCname) {
await GreenlockUtil.removeDomain(domain);
logger.error(`Cname is not valid for domain: ${domain}`);
throw new BadDataException(
"Cname is not valid for domain " + domain,
);
}
if (!isValidCname) {
await GreenlockUtil.removeDomain(domain);
logger.error(`Cname is not valid for domain: ${domain}`);
throw new BadDataException("Cname is not valid for domain " + domain);
}
const client: acme.Client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.production,
accountKey: acmeAccountKey,
});
const client: acme.Client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.production,
accountKey: acmeAccountKey,
});
const [certificateKey, certificateRequest] =
await acme.crypto.createCsr({
commonName: domain,
});
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
commonName: domain,
});
const certificate: string = await client.auto({
csr: certificateRequest,
email: LetsEncryptNotificationEmail.toString(),
termsOfServiceAgreed: true,
challengePriority: ["http-01"], // only http-01 challenge is supported by oneuptime
challengeCreateFn: async (
authz: acme.Authorization,
challenge: Challenge,
keyAuthorization: string,
) => {
// Satisfy challenge here
/* http-01 */
if (challenge.type === "http-01") {
const acmeChallenge: AcmeChallenge = new AcmeChallenge();
acmeChallenge.challenge = keyAuthorization;
acmeChallenge.token = challenge.token;
acmeChallenge.domain = authz.identifier.value;
const certificate: string = await client.auto({
csr: certificateRequest,
email: LetsEncryptNotificationEmail.toString(),
termsOfServiceAgreed: true,
challengePriority: ["http-01"], // only http-01 challenge is supported by oneuptime
challengeCreateFn: async (
authz: acme.Authorization,
challenge: Challenge,
keyAuthorization: string,
) => {
// Satisfy challenge here
/* http-01 */
if (challenge.type === "http-01") {
const acmeChallenge: AcmeChallenge = new AcmeChallenge();
acmeChallenge.challenge = keyAuthorization;
acmeChallenge.token = challenge.token;
acmeChallenge.domain = authz.identifier.value;
await AcmeChallengeService.create({
data: acmeChallenge,
props: {
isRoot: true,
},
});
}
},
challengeRemoveFn: async (
authz: acme.Authorization,
challenge: Challenge,
) => {
// Clean up challenge here
await AcmeChallengeService.create({
data: acmeChallenge,
if (challenge.type === "http-01") {
await AcmeChallengeService.deleteBy({
query: {
domain: authz.identifier.value,
},
limit: 1,
skip: 0,
props: {
isRoot: true,
},
});
}
},
});
// get expires at date from certificate
const cert: acme.CertificateInfo =
acme.crypto.readCertificateInfo(certificate);
const issuedAt: Date = cert.notBefore;
const expiresAt: Date = cert.notAfter;
// check if the certificate is already in the database.
const existingCertificate: AcmeCertificate | null =
await AcmeCertificateService.findOneBy({
query: {
domain: domain,
},
select: {
_id: true,
},
props: {
isRoot: true,
},
});
}
},
challengeRemoveFn: async (
authz: acme.Authorization,
challenge: Challenge,
) => {
// Clean up challenge here
if (challenge.type === "http-01") {
await AcmeChallengeService.deleteBy({
if (existingCertificate) {
// update the certificate
await AcmeCertificateService.updateBy({
query: {
domain: authz.identifier.value,
domain: domain,
},
limit: 1,
skip: 0,
data: {
certificate: certificate.toString(),
certificateKey: certificateKey.toString(),
issuedAt: issuedAt,
expiresAt: expiresAt,
},
props: {
isRoot: true,
},
});
} else {
// create the certificate
const acmeCertificate: AcmeCertificate = new AcmeCertificate();
acmeCertificate.domain = domain;
acmeCertificate.certificate = certificate.toString();
acmeCertificate.certificateKey = certificateKey.toString();
acmeCertificate.issuedAt = issuedAt;
acmeCertificate.expiresAt = expiresAt;
await AcmeCertificateService.create({
data: acmeCertificate,
props: {
isRoot: true,
},
});
}
},
});
// get expires at date from certificate
const cert: acme.CertificateInfo =
acme.crypto.readCertificateInfo(certificate);
const issuedAt: Date = cert.notBefore;
const expiresAt: Date = cert.notAfter;
Telemetry.endSpan(span);
} catch (e) {
logger.error(`Error ordering certificate for domain: ${data.domain}`);
// check if the certificate is already in the database.
const existingCertificate: AcmeCertificate | null =
await AcmeCertificateService.findOneBy({
query: {
domain: domain,
},
select: {
_id: true,
},
props: {
isRoot: true,
},
});
Telemetry.recordExceptionMarkSpanAsErrorAndEndSpan({
span,
exception: e,
});
if (existingCertificate) {
// update the certificate
await AcmeCertificateService.updateBy({
query: {
domain: domain,
},
limit: 1,
skip: 0,
data: {
certificate: certificate.toString(),
certificateKey: certificateKey.toString(),
issuedAt: issuedAt,
expiresAt: expiresAt,
},
props: {
isRoot: true,
},
});
} else {
// create the certificate
const acmeCertificate: AcmeCertificate = new AcmeCertificate();
if (e instanceof Exception) {
throw e;
}
acmeCertificate.domain = domain;
acmeCertificate.certificate = certificate.toString();
acmeCertificate.certificateKey = certificateKey.toString();
acmeCertificate.issuedAt = issuedAt;
acmeCertificate.expiresAt = expiresAt;
await AcmeCertificateService.create({
data: acmeCertificate,
props: {
isRoot: true,
},
});
}
Telemetry.endSpan(span);
} catch (e) {
logger.error(`Error ordering certificate for domain: ${data.domain}`);
Telemetry.recordExceptionMarkSpanAsErrorAndEndSpan({
span,
exception: e,
});
if (e instanceof Exception) {
throw e;
}
throw new ServerException(
`Unable to order certificate for ${data.domain}. Please contact support at support@oneuptime.com for more information.`,
);
}
throw new ServerException(
`Unable to order certificate for ${data.domain}. Please contact support at support@oneuptime.com for more information.`,
);
}
},
});
}
}

View File

@ -33,6 +33,7 @@ import logger from "./Logger";
export type Span = opentelemetry.api.Span;
export type SpanStatus = opentelemetry.api.SpanStatus;
export type SpanException = opentelemetry.api.Exception;
export type SpanOptions = opentelemetry.api.SpanOptions;
export enum SpanStatusCode {
UNSET = 0,
@ -304,14 +305,14 @@ export default class Telemetry {
return tracer;
}
public static startSpan(data: {
public static startActiveSpan<T>(data: {
name: string;
attributes?: opentelemetry.api.Attributes;
}): Span {
const { name, attributes } = data;
options?: SpanOptions | undefined;
fn: (span: Span) => T;
}): T {
const { name } = data;
const span: Span = this.getTracer().startSpan(name, attributes);
return span;
return this.getTracer().startActiveSpan(name, data.options || {}, data.fn);
}
public static recordExceptionMarkSpanAsErrorAndEndSpan(data: {