入门OpenTelemetry——应用自动埋点

埋点

什么是埋点

埋点,本质就是在你的应用程序里,在重要位置插入采集代码,比如:

  • 收集请求开始和结束的时间
  • 收集数据库查询时间
  • 收集函数调用链路信息
  • 收集异常信息

这些埋点数据(Trace、Metrics、Logs)被收集起来后,可以在监控平台看到系统运行时的真实表现,帮助你做:

  • 性能分析
  • 故障排查
  • 调用链路追踪

简单说就是:“在合适的地方插追踪/监控代码”。

要使用 OpenTelemetry 检测应用程序,可以前往访问 OpenTelemetry 存储库,选择适用于的应用程序的语言,然后按照说明进行操作。具体可以参考文档:https://opentelemetry.opendocs.io/docs/getting-started/dev/

自动埋点

使用自动埋点是一个很好的方式,因为它简单、容易,不需要进行很多代码更改。

如果你没有必要的知识(或时间)来创建适合你应用程序量身的追踪代码,那么这种方法就非常合适。

OpenTelemetry 支持自动化埋点的语言:

  • .net
  • Java
  • JavaScript
  • PHP
  • Python

手动检测

手动检测是指为应用程序编写特定的埋点代码。这是向应用程序添加可观测性代码的过程。这样做可以更有效地满足你的需求,因为可以自己添加属性和事件。这样做的缺点是需要导入库并自己完成所有工作。

埋点方式对比

手动埋点(Manual Instrumentation)自动埋点(Automatic Instrumentation)
定义程序员自己在代码里显式写下采集逻辑借助 SDK/Agent 自动拦截应用,无需修改业务代码
实现方式引用 OpenTelemetry API,比如创建 Tracer,手动打 span安装一个 Agent(Java agent、Python instrumentation)自动检测框架和库,插入追踪
控制力度非常高,想怎么打点都可以较低,受限于 Agent 支持的范围
开发成本高,需要自己判断哪里要加埋点低,几乎开箱即用
支持范围业务逻辑细粒度打点,比如特定函数、算法内部框架级打点,比如 HTTP 请求、数据库访问、消息队列消费
性能影响可控,看你打点多少可能稍高,因为 Agent 会 Hook 很多地方
典型场景需要追踪复杂业务逻辑快速上线链路追踪,不想改代码

k8s 应用自动埋点步骤

  • 部署 OpenTelemetry Operator:它帮你管理 InstrumentationOpenTelemetryCollector,实现自动注入、自动采集功能。
  • 部署 OpenTelemetryCollector:用来接收自动埋点产生的数据,比如 traces。
  • 定义 Instrumentation 对象:声明“我想要给哪些应用自动打点”(比如 Java 的 agent),并指定用哪个 Collector
  • 给你的 Pod 加上 Annotation:Operator 会根据 Annotation 自动注入 Agent 和 Sidecar。

自动埋点配置详解

配置示例

apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:name: <name>namespace: <namespace>
spec:exporter:                     # 导出目标配置endpoint: <string>          # 指定导出的地址,通常是 OpenTelemetry Collector 的 OTLP 接收端口tls:											  # 是否使用非加密连接(跳过 TLS)insecure: <bool>          # 跳过 TLS 校验,默认 falseinsecureSkipVerify: <bool>propagators:								  # 上下文传播协议,如果是跨服务追踪,一定要所有服务使用同一传播协议- tracecontext              # W3C Trace Context 标准(推荐)- baggage                   #	W3C Baggage(支持传递 key-value)- b3                        #	B3 single-header(Zipkin 风格)- b3multi                   # B3 multi-header- jaeger                    # Jaeger 原生格式sampler:									  	# 采样器配置type: <sampler_type>argument: <string>        resource:											# 资源标签,可选配置attributes:service.name: <string>      # 用于区分不同服务service.namespace: <string> # 服务所属 namespaceservice.version: <string>   # 服务版本env:												  	# 全局环境变量- name: OTEL_FOOvalue: "bar"             

采样器配置

采样器配置如下:

类型含义是否支持 argument
always_on全部采样
always_off全部不采样
traceidratio指定比例采样是(如 "0.25"
parentbased_traceidratio如果上游有 trace,继承上游;否则按照比例采样
parentbased_always_on如果上游有 trace,继承;否则全部采样
parentbased_always_off如果上游有 trace,继承;否则不采样

argument 字段通常是小数,表示采样概率,如 "1" 表示 100%,"0.5" 表示 50%。

其他配置

其他配置可通过环境变量方式注入,具体配置项可参考文档:

https://opentelemetry.io/docs/languages/sdk-configuration/

部署示例应用

部署 java 应用

这里我们将使用一个名为 Petclinic 的 Java 应用程序,这是一个使用 Maven 或 Gradle 构建的 Spring Boot 应用程序。该应用程序将使用 OpenTelemetry 生成数据。

Petclinic 示例项目地址:https://github.com/spring-projects/spring-petclinic

apiVersion: apps/v1
kind: Deployment
metadata:name: java-demo
spec:selector:matchLabels:app: java-demotemplate:metadata:labels:app: java-demospec:containers:- name: java-demoimage: contrastsecuritydemo/spring-petclinic:1.5.1imagePullPolicy: IfNotPresentresources:limits:memory: "500Mi"cpu: "200m"ports:- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:name: java-demo
spec:selector:app: java-demoports:- port: 8080targetPort: 8080
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:name: java-demo
spec:entryPoints:- webroutes:- match: Host(`java-demo.cuiliangblog.cn`)kind: Ruleservices:- name: java-demoport: 8080

部署完成后通过域名访问验证。

部署 python 应用

这里我们将使用一个名为 python-demoapp 的 python 应用程序,这是一个使用 flask 构建的 web 应用程序。该应用程序将使用 OpenTelemetry 生成数据。

项目地址:https://github.com/benc-uk/python-demoapp

apiVersion: apps/v1
kind: Deployment
metadata:name: python-demo
spec:selector:matchLabels:app: python-demotemplate:metadata:labels:app: python-demospec:containers:- name: python-demoimage: ghcr.io/benc-uk/python-demoapp:latestimagePullPolicy: IfNotPresentresources:limits:memory: "500Mi"cpu: "200m"ports:- containerPort: 5000
---
apiVersion: v1
kind: Service
metadata:name: python-demo
spec:selector:app: python-demoports:- port: 5000targetPort: 5000
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:name: python-demo
spec:entryPoints:- webroutes:- match: Host(`python-demo.local.com`)kind: Ruleservices:- name: python-demoport: 5000

接下来通过域名访问应用

应用埋点

java 应用自动埋点

对于 Java 应用,我们可以通过下载 OpenTelemetry 提供的 opentelemetry-javaagent 这个 jar 包来使用 OpenTelemetry 自动检测应用程序。

opentelemetry-javaagent 地址:https://github.com/open-telemetry/opentelemetry-java-instrumentation

只需要将这个 jar 包添加到应用程序的启动命令中即可,比如:

java -javaagent:opentelemetry-javaagent.jar -jar target/*.jar

Java 自动检测使用可附加到任何 Java 8+ 应用程序的 Java 代理 JAR。它动态注入字节码以从许多流行的库和框架捕获遥测数据。它可用于捕获应用程序或服务“边缘”的遥测数据,例如入站请求、出站 HTTP 调用、数据库调用等。通过运行以上命令,我们可以对应用程序进行插桩,并生成链路数据,而对我们的应用程序没有任何修改。
尤其是在 Kubernetes 环境中,我们可以使用 OpenTelemetry Operator 来注入和配置 OpenTelemetry 自动检测库,这样连 javaagent 我们都不需要去手动注入了。
接下来为 Java 应用程序添加一个 Instrumentation 资源。

apiVersion: opentelemetry.io/v1alpha1    
kind: Instrumentation                     # 声明资源类型为 Instrumentation(用于语言自动注入)
metadata:name: java-instrumentation              # Instrumentation 资源的名称(可以被 Deployment 等引用)namespace: opentelemetry
spec:propagators:                            # 指定用于 trace 上下文传播的方式,支持多种格式- tracecontext                        # W3C Trace Context(最通用的跨服务追踪格式)- baggage                             # 传播用户定义的上下文键值对- b3                                  # Zipkin 的 B3 header(用于兼容 Zipkin 环境)sampler:                                # 定义采样策略(决定是否收集 trace)type: always_on                       # 始终采样所有请求(适合测试或调试环境)java:# image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:latest# 使用的 Java 自动注入 agent 镜像地址image:  harbor.cuiliangblog.cn/otel/autoinstrumentation-java:latestenv:- name: OTEL_EXPORTER_OTLP_ENDPOINTvalue: http://center-collector.opentelemetry.svc:4318

为了启用自动检测,我们需要更新部署文件并向其添加注解。这样我们可以告诉 OpenTelemetry Operator 将 sidecar 和 java-instrumentation 注入到我们的应用程序中。修改 Deployment 配置如下:

apiVersion: apps/v1
kind: Deployment
metadata:name: java-demo
spec:selector:matchLabels:app: java-demotemplate:metadata:labels:app: java-demoannotations:instrumentation.opentelemetry.io/inject-java: "opentelemetry/java-instrumentation" # 填写 Instrumentation 资源的名称sidecar.opentelemetry.io/inject: "opentelemetry/sidecar" # 注入一个 sidecar 模式的 OpenTelemetry Collectorspec:containers:- name: java-demoimage: contrastsecuritydemo/spring-petclinic:1.5.1imagePullPolicy: IfNotPresentresources:limits:memory: "500Mi"cpu: "200m"ports:- containerPort: 8080

接下来更新 deployment,然后查看资源信息,java-demo 容器已经变为两个。

# kubectl get pod
NAME                        READY   STATUS    RESTARTS      AGE
java-demo-557fff6b7c-x8tjg  2/2     Running   0               3m6s
# kubectl get opentelemetrycollectors -A                 
NAMESPACE       NAME      MODE         VERSION   READY   AGE   IMAGE                                                                                     MANAGEMENT
default         sidecar   sidecar      0.123.1           39m                                                                                             managed
opentelemetry   simple    deployment   0.123.1   1/1     39m   ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector:0.123.1   managed
# kubectl get instrumentations -A                 
NAMESPACE       NAME                   AGE   ENDPOINT   SAMPLER     SAMPLER ARG
opentelemetry   java-instrumentation   39m              always_on 

查看 sidecar日志,已正常启动并发送 spans 数据

# kubectl logs java-demo-557fff6b7c-x8tjg -c otc-container
2025-04-23T08:56:32.664Z        info    grpc@v1.71.0/server.go:690      [core] [Server #3]Server created        {"grpc_log": true}
2025-04-23T08:56:32.669Z        info    otlpreceiver@v0.123.0/otlp.go:116       Starting GRPC server    {"endpoint": "0.0.0.0:4317"}
2025-04-23T08:56:32.670Z        info    otlpreceiver@v0.123.0/otlp.go:173       Starting HTTP server    {"endpoint": "0.0.0.0:4318"}
2025-04-23T08:56:32.670Z        info    service@v0.123.0/service.go:287 Everything is ready. Begin running and processing data.
2025-04-23T08:56:32.670Z        info    grpc@v1.71.0/server.go:886      [core] [Server #3 ListenSocket #4]ListenSocket created     {"grpc_log": true}
2025-04-23T08:56:32.686Z        info    grpc@v1.71.0/clientconn.go:1224 [core] [Channel #1 SubChannel #2]Subchannel Connectivity change to READY   {"grpc_log": true}
2025-04-23T08:56:32.686Z        info    pickfirst/pickfirst.go:184      [pick-first-lb] [pick-first-lb 0xc000ab7530] Received SubConn state update: 0xc0008b6550, {ConnectivityState:READY ConnectionError:<nil> connectedAddress:{Addr:simple-collector.opentelemetry.svc:4317 ServerName:simple-collector.opentelemetry.svc:4317 Attributes:<nil> BalancerAttributes:<nil> Metadata:<nil>}}    {"grpc_log": true}
2025-04-23T08:56:32.686Z        info    grpc@v1.71.0/clientconn.go:563  [core] [Channel #1]Channel Connectivity change to READY    {"grpc_log": true}
2025-04-23T08:57:26.022Z        info    Traces  {"resource spans": 1, "spans": 72}
2025-04-23T08:57:36.027Z        info    Traces  {"resource spans": 1, "spans": 4}

查看collector 日志,已经收到 traces 数据

# kubectl logs -n opentelemetry simple-collector-5b5699b46f-qgdw6
2025-04-23T07:28:27.220Z        info    service@v0.123.0/service.go:197 Setting up own telemetry...
2025-04-23T07:28:27.220Z        info    builders/builders.go:26 Development component. May change in the future.
2025-04-23T07:28:27.223Z        info    memorylimiter@v0.123.0/memorylimiter.go:148     Using percentage memory limiter    {"total_memory_mib": 7914, "limit_percentage": 75, "spike_limit_percentage": 15}
2025-04-23T07:28:27.223Z        info    memorylimiter@v0.123.0/memorylimiter.go:74      Memory limiter configured {"limit_mib": 5935, "spike_limit_mib": 1187, "check_interval": 1}
2025-04-23T07:28:27.270Z        info    service@v0.123.0/service.go:264 Starting otelcol...     {"Version": "0.123.1", "NumCPU": 4}
2025-04-23T07:28:27.270Z        info    extensions/extensions.go:41     Starting extensions...
2025-04-23T07:28:27.271Z        info    otlpreceiver@v0.123.0/otlp.go:116       Starting GRPC server    {"endpoint": "0.0.0.0:4317"}
2025-04-23T07:28:27.271Z        info    otlpreceiver@v0.123.0/otlp.go:173       Starting HTTP server    {"endpoint": "0.0.0.0:4318"}
2025-04-23T07:28:27.272Z        info    service@v0.123.0/service.go:287 Everything is ready. Begin running and processing data.
2025-04-23T08:57:26.022Z        info    Traces  {"resource spans": 1, "spans": 72}
2025-04-23T08:57:36.027Z        info    Traces  {"resource spans": 1, "spans": 4}

python 应用自动埋点

与 java 应用类似,python 应用同样也支持自动埋点, OpenTelemetry 提供了 opentelemetry-instrument CLI 工具,在启动 Python 应用时通过 sitecustomize 或环境变量注入自动 instrumentation。

我们先创建一个java-instrumentation 资源

apiVersion: opentelemetry.io/v1alpha1    
kind: Instrumentation                     # 声明资源类型为 Instrumentation(用于语言自动注入)
metadata:name: python-instrumentation              # Instrumentation 资源的名称(可以被 Deployment 等引用)namespace: opentelemetry
spec:propagators:                            # 指定用于 trace 上下文传播的方式,支持多种格式- tracecontext                        # W3C Trace Context(最通用的跨服务追踪格式)- baggage                             # 传播用户定义的上下文键值对- b3                                  # Zipkin 的 B3 header(用于兼容 Zipkin 环境)sampler:                                # 定义采样策略(决定是否收集 trace)type: always_on                       # 始终采样所有请求(适合测试或调试环境)python:image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python:latestenv:                                  - name: OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED # 启用日志的自动检测value: "true"- name: OTEL_PYTHON_LOG_CORRELATION # 在日志中启用跟踪上下文注入value: "true"- name: OTEL_EXPORTER_OTLP_ENDPOINTvalue: http://center-collector.opentelemetry.svc:4318

然后更新 deployment 资源清单,添加注解

apiVersion: apps/v1
kind: Deployment
metadata:name: python-demo
spec:selector:matchLabels:app: python-demotemplate:metadata:labels:app: python-demoannotations:instrumentation.opentelemetry.io/inject-python: "opentelemetry/python-instrumentation" # 填写 Instrumentation 资源的名称sidecar.opentelemetry.io/inject: "opentelemetry/sidecar" # 注入一个 sidecar 模式的 OpenTelemetry Collector……

接下来观察日志既可。

查看更多

崔亮的博客-专注devops自动化运维,传播优秀it运维技术文章。更多原创运维开发相关文章,欢迎访问https://www.cuiliangblog.cn

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/81410.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大数据场景下数据导出的架构演进与EasyExcel实战方案

一、引言&#xff1a;数据导出的演进驱动力 在数字化时代&#xff0c;数据导出功能已成为企业数据服务的基础能力。随着数据规模从GB级向TB级甚至PB级发展&#xff0c;传统导出方案面临三大核心挑战&#xff1a; ‌数据规模爆炸‌&#xff1a;单次导出数据量从万级到亿级的增长…

拓展运算符与数组解构赋值的区别

拓展运算符与数组解构赋值是ES6中用于处理数组的两种不同的特性&#xff0c;它们有以下区别&#xff1a; 概念与作用 • 拓展运算符&#xff1a;主要用于将数组展开成一系列独立的元素&#xff0c;或者将多个数组合并为一个数组&#xff0c;以及在函数调用时将数组作为可变参…

2025年全国青少年信息素养大赛初赛真题(算法创意实践挑战赛C++初中组:文末附答案)

2025年全国青少年信息素养大赛初赛真题(算法创意实践挑战赛C++初中组:文末附答案) 一、单项选择题(每题 5 分) C++ 程序流程控制的基本结构不包括以下哪项? A. 分支结构 B. 数据结构 C. 循环结构 D. 顺序结构 以下哪段代码能将数组 int a[4] = {2, 4, 6, 8}; 的所有元素变…

计算机视觉与深度学习 | Python实现EMD-CNN-LSTM时间序列预测(完整源码、数据、公式)

EMD-CNN-LSTM 1. 环境准备2. 数据生成(示例数据)3. EMD分解4. 数据预处理5. CNN-LSTM模型定义6. 模型训练7. 预测与重构8. 性能评估核心公式说明1. 经验模态分解(EMD)2. CNN-LSTM混合模型参数调优建议扩展方向典型输出示例以下是使用Python实现EMD-CNN-LSTM时间序列预测的完…

React 19中useContext不需要Provider了。

文章目录 前言一、React 19中useContext移除了Provider&#xff1f;二、使用步骤总结 前言 在 React 19 中&#xff0c;useContext 的使用方式有所更新。开发者现在可以直接使用 作为提供者&#xff0c;而不再需要使用 <Context.Provider>。这一变化简化了代码结构&…

单片机-STM32部分:14、SPI

飞书文档https://x509p6c8to.feishu.cn/wiki/VYYnwOc9Zi6ibFk36lYcPQdRnlf 什么是SPI SPI 是英语Serial Peripheral interface的缩写&#xff0c;顾名思义就是串行外围设备接口。是Motorola(摩托罗拉)首先在其MC68HCXX系列处理器上定义的。 SPI&#xff0c;是一种高速的&…

Vue 3 动态 ref 的使用方式(表格)

一、问题描述 先给大家简单介绍一下问题背景。我正在开发的项目中&#xff0c;有一个表格组件&#xff0c;其中一列是分镜描述&#xff0c;需要支持视频上传功能。用户可以为每一行的分镜描述上传对应的视频示例。然而&#xff0c;在实现过程中&#xff0c;出现了一个严重的问…

构建 TypoView:一个富文本样式预览工具的全流程记录

我正在参加CodeBuddy「首席试玩官」内容创作大赛&#xff0c;本文所使用的 CodeBuddy 免费下载链接&#xff1a;腾讯云代码助手 CodeBuddy - AI 时代的智能编程伙伴 在一次和 CodeBuddy 的日常交流中&#xff0c;我提出了一个构想&#xff1a;能不能帮我从零构建一个富文本样式…

AI:OpenAI论坛分享—《AI重塑未来:技术、经济与战略》

AI&#xff1a;OpenAI论坛分享—《AI重塑未来&#xff1a;技术、经济与战略》 导读&#xff1a;2025年4月24日&#xff0c;OpenAI论坛全面探讨了 AI 的发展趋势、技术范式、地缘政治影响以及对经济和社会的广泛影响。强调了 AI 的通用性、可扩展性和高级推理能力&#xff0c;以…

Bash fork 炸弹 —— :(){ :|: };:

&#x1f9e0; 什么是 Fork 炸弹&#xff1f; Fork 炸弹是一种拒绝服务&#xff08;DoS&#xff09;攻击技术&#xff0c;利用操作系统的 fork() 系统调用不断创建新进程&#xff0c;直到系统资源&#xff08;如进程表、CPU、内存&#xff09;被耗尽&#xff0c;从而使系统无法…

<前端小白> 前端网页知识点总结

HTML 标签 1. 标题标签 h1到h6 2. 段落标签 p 3. 换行 br 水平线 hr 4. 加粗 strong 倾斜 em 下划线 ins 删除 del 5. 图像标签 img src-图像的位置 alt- 图片加载失败显示的文字 替换文本 title--- 鼠标放到图片上显示的文字 提示…

tomcat查看状态页及调优信息

准备工作 先准备一台已经安装好tomcat的虚拟机&#xff0c;tomcat默认是状态页是默认被禁用的 1.添加授权用户 vim /usr/local/tomcat/conf/tomcat-users.xml22 <role rolename"manager-gui"/>23 <user username"admin" password"tomcat&q…

.NET NativeAOT 指南

目录 1. 引言 2. 什么是 .NET NativeAOT&#xff1f; 2.1 NativeAOT 的定义 2.2 NativeAOT 与传统 JIT 的对比 2.3 NativeAOT 的适用场景 3. NativeAOT 的核心优势 3.1 性能提升 3.2 简化部署 3.3 更小的应用体积 3.4 知识产权保护 4. NativeAOT 的基本用法 4.1 环境…

产品周围的几面墙

不能把排序&#xff0c;当单选题做。 2025年的杭州咖啡馆&#xff0c;味道最浓的不是咖啡&#xff0c;是聊各种项目和创业的卷味。 在过去几年&#xff0c;聊项目的也不少&#xff0c;那时候带着更加浓烈的自信和松弛感&#xff0c;不过今年略带几分忐忑和试探的口吻。 看到网…

例举3种强制类型转换和2种隐式

1. 强制类型转换 强制类型转换是指程序员显式地将一个数据类型的值转换为另一种数据类型。这种转换通常是通过使用特定的函数或运算符来完成的。 常用的强制类型转换方法&#xff1a; 使用Number()函数 let value "123"; let num Number(value); // 强制转换为数字…

UI-TARS本地部署

UI-TARS本地部署 UI-TARS本地部署 UI-TARS 论文&#xff08;arXiv&#xff09; UI-TARS 官方仓库&#xff1a;包含部署指南、模型下载链接及示例代码。 UI-TARS-Desktop 客户端&#xff1a;支持本地桌面应用的交互控制。 模型部署框架&#xff1a;vLLM本地部署 1.下载项目…

新电脑软件配置三 pycharm

快捷键放大和缩小字体 按住ctrl鼠标滚轮向上 缩小同理

华为OD机试真题——考勤信息(2025A卷:100分)Java/python/JavaScript/C/C++/GO最佳实现

2025 A卷 100分 题型 本专栏内全部题目均提供Java、python、JavaScript、C、C++、GO六种语言的最佳实现方式; 并且每种语言均涵盖详细的问题分析、解题思路、代码实现、代码详解、3个测试用例以及综合分析; 本文收录于专栏:《2025华为OD真题目录+全流程解析+备考攻略+经验分…

Python语法规则:缩进、代码块与空格规范

在众多编程语言中&#xff0c;Python 以其“简洁而优雅”的语法风格独树一帜。然而&#xff0c;这种“简洁”并非轻率随意&#xff0c;而是建立在一套严谨的语法哲学之上。缩进、代码块与空格规范&#xff0c;不仅是 Python 的语法基础&#xff0c;更是它传达代码意图、塑造开发…

Baklib智能知识管理增效方案

Baklib智能知识管理核心优势 基于Baklib构建的知识中台&#xff0c;通过多维度结构化处理与智能语义引擎&#xff0c;重构了企业知识管理范式。该系统支持文档、表格、音视频等多格式内容聚合&#xff0c;利用自然语言处理技术实现知识资产的自动化分类与标签匹配&#xff0c;…